├── .build └── .gitignore ├── .gitignore ├── .htaccess ├── CNAME ├── Makefile ├── README.md ├── _config.yml ├── _includes ├── download-detect.html ├── download.html ├── extensions.html ├── footer.html ├── head.html ├── header.html ├── mailing.html ├── piwik.html ├── select_country.html ├── signature.html ├── single-download.html └── toc.md ├── _layouts ├── default.html ├── documentation.html ├── page.html └── post.html ├── _posts └── 2018-10-09-turtl-v07-update.md ├── _scripts ├── postcss.js ├── publish.sh ├── version └── watch ├── contact.html ├── contributing ├── ecla.md ├── icla.md ├── index.md ├── sign-error.md └── sign-thanks.md ├── css └── main.less ├── docs ├── architecture.md ├── index.md ├── security │ ├── encryption-specifics.md │ ├── index.md │ └── stay-logged-in.md ├── syncing.md └── troubleshooting │ └── logging-in.md ├── donate-thanks.md ├── donate.html ├── download.html ├── faq.md ├── features.html ├── images ├── favicon.128.png ├── favicon.png ├── font │ └── fontello.woff2 ├── home │ ├── devices.png │ ├── notebook.jpg │ ├── photo │ │ ├── collaborate.jpg │ │ ├── organized.jpg │ │ └── security.jpg │ └── screen │ │ ├── export.png │ │ ├── invite.png │ │ ├── markdown.png │ │ ├── math.png │ │ ├── notes.jpg │ │ ├── search.jpg │ │ └── spaces.png ├── logo-beta.png ├── logo.svg └── posts │ └── turtl.jpg ├── index.html ├── js ├── bluebird.min.js ├── composer.js ├── controller-sig.js ├── delete-account.js ├── main.js ├── manage.js ├── mootools-core-1.6.0.js ├── mootools-more-1.6.0.js ├── sexhr.js ├── stripe.js ├── uikit-icons.min.js └── uikit.min.js ├── keybase.txt ├── manage └── index.html ├── package.json ├── premium └── index.html ├── privacy.md ├── releases └── .gitignore └── users ├── confirm ├── error.md └── success.md └── delete ├── error.md ├── index.html └── success.md /.build/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vars.mk 2 | _site 3 | .sass-cache 4 | css/*.css 5 | !css/reset.css 6 | /node_modules 7 | /google*.html 8 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine on 3 | RewriteRule ^([a-z-]+)/page/[0-9]+ /$1 [L,R=301] 4 | RewriteRule ^docs/security/remember-me /docs/security/stay-logged-in/ [L,R=301] 5 | RewriteRule ^(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ $1$2 [L] 6 | 7 | 8 | 9 | 10 | Header set Cache-Control "max-age=2592000, public" 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | gh.turtlapp.com -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PUBLISH_TARGET = 2 | 3 | # non-versioned include 4 | -include vars.mk 5 | 6 | JEKYLL := $(shell which jekyll) 7 | NODE := $(shell which node) 8 | LESSC := node_modules/.bin/lessc 9 | POSTCSS := _scripts/postcss.js 10 | VERSION_SCRIPT := _scripts/version 11 | 12 | DESKTOP_VERSION = $(shell cat ../desktop/package.json \ 13 | | grep '"version"' \ 14 | | sed 's|.*: \+"\([^"]\+\)".*|\1|') 15 | MOBILE_VERSION = $(shell cat ../mobile/config.xml \ 16 | | grep '^" $@ 31 | @$(LESSC) --include-path='css/:node_modules/uikit/src/less/components' $< > $@ 32 | 33 | .build/postcss: $(allcss) $(cssfiles) 34 | @echo "- postcss:" $? 35 | @$(NODE) $(POSTCSS) --use autoprefixer --replace $? 36 | @touch $@ 37 | 38 | release-all: 39 | @echo -ne "\n\n--- Building desktop release $(DESKTOP_VERSION) ---\n\n" 40 | @sleep 2 41 | cd ../desktop && make release 42 | @cp ../desktop/release/turtl-* ./releases/desktop 43 | $(VERSION_SCRIPT) desktop $(DESKTOP_VERSION) 44 | 45 | @echo -ne "\n\n--- Building mobile release $(MOBILE_VERSION) ---\n\n" 46 | @sleep 2 47 | cd ../mobile && make release-android && make release-fdroid 48 | @cp ../mobile/platforms/android/build/outputs/apk/android-armv7-release.apk ./releases/mobile/turtl-android.apk 49 | $(VERSION_SCRIPT) mobile $(MOBILE_VERSION) 50 | 51 | @echo -ne "\n\n--- Building jekyl site ---\n\n" 52 | @make 53 | 54 | publish: 55 | _scripts/publish.sh $(PUBLISH_TARGET) 56 | 57 | clean: 58 | rm $(allcss) 59 | rm -f .build/* 60 | 61 | watch: 62 | @./_scripts/watch 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Turtl www 2 | ========= 3 | 4 | _Opening an issue? See the [Turtl project tracker](https://github.com/turtl/project-tracker/issues)_ 5 | 6 | This is Turtl's marketing site. It's build using Jekyll. 7 | 8 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Site settings 2 | asset_version: 10 3 | title: Turtl 4 | description: The safe way to remember everything. Take all your important data with you. 5 | baseurl: "" # the subpath of your site, e.g. /blog/ 6 | url: "https://turtlapp.com" 7 | permalink: "/:year/:month/:title/" 8 | enable_premium: true 9 | stripe_pubkey: "pk_live_61JSZue0As9lbGB2CZK0DSRU" 10 | 11 | # Build settings 12 | markdown: kramdown 13 | kramdown: 14 | parse_block_html: true 15 | auto_ids: true 16 | toc_levels: 1..3 17 | 18 | exclude: 19 | - Makefile 20 | - package.json 21 | - node_modules/ 22 | - "*.less" 23 | 24 | ################################################################################ 25 | # this section is auto-generated. please use megarelease. 26 | ################################################################################ 27 | releases: 28 | core: '0.1.1' 29 | desktop: '0.7.2.5' 30 | android: '0.7.2.5' 31 | ext: '0.7.0' 32 | ################################################################################ 33 | 34 | -------------------------------------------------------------------------------- /_includes/download-detect.html: -------------------------------------------------------------------------------- 1 | {% if include.hide %} 2 | {% include download.html class="detect" %} 3 | 4 | 5 | {% else %} 6 | {% include download.html %} 7 | {% endif %} 8 | 9 | -------------------------------------------------------------------------------- /_includes/download.html: -------------------------------------------------------------------------------- 1 |
2 | {% capture link %}https://github.com/turtl/desktop/releases/download/v{{site.releases.desktop}}/turtl-{{site.releases.desktop}}-windows64.msi{% endcapture %} 3 | {% capture title %}Windows desktop v{{site.releases.desktop}}{% endcapture %} 4 | {% include single-download.html link=link title=title icon="F17A" text="Windows x64" %} 5 | 6 | {% capture link %}https://github.com/turtl/desktop/releases/download/v{{site.releases.desktop}}/turtl-{{site.releases.desktop}}-osx.zip{% endcapture %} 7 | {% capture title %}OSx desktop v{{site.releases.desktop}}{% endcapture %} 8 | {% include single-download.html link=link title=title icon="F179" text="OSx x64" %} 9 | 10 | {% capture link %}https://github.com/turtl/desktop/releases/download/v{{site.releases.desktop}}/turtl-{{site.releases.desktop}}-linux64.tar.bz2{% endcapture %} 11 | {% capture title %}Linux 64-bit desktop v{{site.releases.desktop}}{% endcapture %} 12 | {% include single-download.html link=link title=title icon="F17C" text="Linux x64" %} 13 | 14 | {% capture link %}https://github.com/turtl/desktop/releases/download/v{{site.releases.desktop}}/turtl-{{site.releases.desktop}}-linux32.tar.bz2{% endcapture %} 15 | {% capture title %}Linux 32-bit desktop v{{site.releases.desktop}}{% endcapture %} 16 | {% include single-download.html link=link title=title icon="F17C" text="Linux x32" %} 17 | 18 | {% capture link %}https://play.google.com/store/apps/details?id=com.lyonbros.turtl{% endcapture %} 19 | {% capture title %}Android v{{site.releases.desktop}}{% endcapture %} 20 | {% include single-download.html link=link title=title icon="F17B" text="Android" %} 21 | 22 | {% capture link %}https://github.com/turtl/android/releases/download/v{{site.releases.android}}/turtl-{{site.releases.desktop}}-android.apk{% endcapture %} 23 | {% capture title %}Android v{{site.releases.android}} (.apk){% endcapture %} 24 | {% include single-download.html link=link title=title icon="F17B" text="Android (.apk)" %} 25 | 26 | {% capture title %}iOS v{{site.releases.desktop}}{% endcapture %} 27 | {% include single-download.html disable=1 title=title icon="E840" text="iOS (Coming soon)" %} 28 |
29 | 30 | -------------------------------------------------------------------------------- /_includes/extensions.html: -------------------------------------------------------------------------------- 1 |
2 | {% capture link %}https://chrome.google.com/webstore/detail/turtl/dgcojenhfdjhieoglmiaheihjadlpcml{% endcapture %} 3 | {% capture title %}Chrome extension v{{site.ext_version}}{% endcapture %} 4 | {% include single-download.html link=link title=title icon="F268" text="Chrome" %} 5 | 6 | {% capture link %}https://addons.mozilla.org/en-US/firefox/addon/turtl-bookmarking/{% endcapture %} 7 | {% capture title %}Firefox extension v{{site.ext_version}}{% endcapture %} 8 | {% include single-download.html link=link title=title icon="F269" text="Firefox" %} 9 |
10 | 11 | -------------------------------------------------------------------------------- /_includes/footer.html: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title }} | {% endif %}{{ site.title }} 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /_includes/select_country.html: -------------------------------------------------------------------------------- 1 | 217 | 218 | -------------------------------------------------------------------------------- /_includes/signature.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | {% if include.sigtype == "ecla" %} 7 | 8 | 9 | 14 | 15 | {% endif %} 16 | 17 | 18 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 50 | 51 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | 87 | 90 | 91 |
*Company/Organization/Entity name 10 |
11 | 12 |
13 |
*Full name 19 |
20 | 21 |
22 |
*Email 27 |
28 | 29 |
30 |
*Mailing address 35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 | {% include select_country.html tabindex=8 %} 48 |
49 |
*Github username 64 |
65 | 66 |
67 |
Website (please leave this field blank) 72 |
73 | 74 |
75 |
*Electronic signature: Type "I AGREE" to accept the terms above 80 |
81 | 82 |
83 |
  88 | 89 |
92 |
    93 |
    94 |
    95 | 96 | -------------------------------------------------------------------------------- /_includes/single-download.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if include.disable %}{% else %}{% endif %} 6 |
    7 | 8 | -------------------------------------------------------------------------------- /_includes/toc.md: -------------------------------------------------------------------------------- 1 | {:.no_toc} 2 | 3 |
    4 | * TOC! 5 | {:toc} 6 |
    7 | 8 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {% include header.html %} 6 |
    7 | 8 | {{ content }} 9 | 10 |
    11 | {% include footer.html %} 12 | 13 | {% include piwik.html %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /_layouts/documentation.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 | 5 |
    6 | {{content}} 7 |
    8 | -------------------------------------------------------------------------------- /_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
    5 |
    6 | 7 | {{content}} 8 | 9 |
    10 |
    11 | 12 | -------------------------------------------------------------------------------- /_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 |
    5 |
    6 |

    {{ page.title }}

    7 |
    8 | {{ page.date | date: '%B %d, %Y' }} 9 |
    10 |
    11 | {% if page.post_header %} 12 |
    13 | 14 |
    15 | {% endif %} 16 | 17 | {{content}} 18 |
    19 | 20 | -------------------------------------------------------------------------------- /_posts/2018-10-09-turtl-v07-update.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 'Turtl v0.7 Update' 4 | post_header: 'turtl.jpg' 5 | --- 6 | 7 | Hi, everyone. We're excited to announce the release of Turtl v0.7. We've been 8 | slowly testing and tuning it for many months now with the help of some dedicated 9 | contributors, and it's finally ready for launch! 10 | 11 |
    12 | Download Turtl v0.7 13 |
    14 | 15 | This is a really big update to Turtl, and deserves some explanation. 16 | 17 | We've comletely rebuilt the server from scratch. What was once Common Lisp is 18 | now Javascript (the new server [lives here](https://github.com/turtl/server) for 19 | those interested). 20 | 21 | We've also rebuilt/rearchitected a lot of the internals of Turtl as well. A lot 22 | of this has to do with making Turtl easier to use with teams, but has also made 23 | everything much faster and more stable. The app itself now takes seconds to do 24 | things that used to take minutes. On top of this, we're now using a more secure 25 | method to protect your data. 26 | 27 | ## Account migration 28 | 29 | Because so much has changed since the last version, an account migration is 30 | required. Also, new accounts no longer use anonymous usernames and will require 31 | an email. I know this feels like a drastic change, but it's incredibly hard to 32 | help people if all they have handy is a non-public username. If you really want 33 | to remain anonymous, you can use a fake email because _confirmation of email 34 | accounts is only required to enable sharing._ This works very closely to how 35 | personas operated in the past. 36 | 37 | Account migration happens in the app itself. It downloads your profile off of 38 | the old server, reformats it for the new server, and publishes it to your 39 | newly-created account. The process should be straightforward, but feel free to 40 | [reach out](/contact) if you have problems. There were some issues with account 41 | migration on android on v0.7.0, but v0.7.1 has been released which fixes these 42 | issues. 43 | 44 | Please note that migration does not move shares from the previous version. To 45 | share on the new version of Turtl, the person you want to share with also needs 46 | to be on the new version of Turtl and you must re-share with them. 47 | 48 | ### Known issues 49 | 50 | Migration is a complicated process and while we did a lot of testing before 51 | launching the new version, some people are experiencing problems: 52 | 53 | - ~~Some users are experiencing a problem logging in with their old credentials 54 | ([#190](https://github.com/turtl/tracker/issues/190))~~ 55 | - ~~Some users are experiencing an issue where migration is not moving any of 56 | their data over ([#192](https://github.com/turtl/tracker/issues/192))~~ 57 | - Your saved servers are lost when upgrading. If you were connecting to a 58 | non-default server, [you will have to enter it again](/docs/troubleshooting/logging-in#custom-servers). 59 | 60 | If you have a problem with migration, don't worry, your data is safe on the old 61 | servers. The migration process *copies* your old data, it doesn't actually 62 | change it. Please bear with us as we fix these problems. 63 | 64 | Also, feel free to install the old version of Turtl until we have everything 65 | sorted out: 66 | 67 | - Desktop: 68 | - Android: 69 | 70 | ## Other changes 71 | 72 | Turtl now has the concept of Spaces. These act as containers for your data that 73 | keep things separate from each other. For instance, you might have a "Personal" 74 | space or a "Work" space. Spaces enable sharing between users...we still have 75 | boards, but they don't allow sharing anymore. We might allow sharing for 76 | individual boards in the future (see [this issue](https://github.com/turtl/tracker/issues/185)). 77 | Spaces allow more granular permissions with sharing, which should make using in 78 | team-based settings much easier. 79 | 80 | As mentioned, Turtl still uses boards. However, boards cannot be nested under 81 | other boards anymore. I know this might upset some, but the feature was not 82 | used much and complicated the interface and architecture quite a bit. 83 | 84 | Notes can now exist in at most one board. This was another feature that 85 | complicated things a lot but we felt didn't add much. 86 | 87 | ## Technical stuff 88 | 89 | Turtl v0.7 now uses a [new component called the Core](https://github.com/turtl/core-rs) 90 | which replaces most of the old app's code. The core is built in Rust and handles 91 | all the syncing, sharing, and cryptography, allowing the [js project](https://github.com/turtl/js) 92 | to act purely as an interface/UI and not house any logic. 93 | 94 | When building the Turtl project from scratch, you'll need to follow instructions 95 | for building the core, the js project, and whatever final platform you're 96 | building for (desktop/android). 97 | 98 | ## Up next: iOS 99 | 100 | One of our next big goals is an iOS app. The new core component makes this much 101 | easier and while we don't have a specific timeline, we'll try to be as 102 | transparent as possible about updates. 103 | 104 | ## Contributing 105 | 106 | The Turtl project used to use Trello to track our progress, but we've since 107 | moved to a centralized [github tracker](https://github.com/turtl/tracker) to 108 | organize all of our bugs and feature requests. 109 | 110 | Please see our new [contributing page](/contributing) for more info on helping 111 | out with the project! 112 | 113 | (Post photo credit: Jose Aragones) 114 | 115 | -------------------------------------------------------------------------------- /_scripts/postcss.js: -------------------------------------------------------------------------------- 1 | require('es6-promise').polyfill(); 2 | require('postcss-cli'); 3 | 4 | -------------------------------------------------------------------------------- /_scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEST=$1 4 | 5 | 6 | if [ "$DEST" == "" ]; then 7 | echo "Usage: $0 " 8 | echo "" 9 | echo " Example $0 jo@mama.com:/tmp/turtl-site" 10 | exit 1 11 | fi 12 | 13 | rsync \ 14 | -avz \ 15 | --no-perms --no-owner --no-group \ 16 | --delete \ 17 | --delete-excluded \ 18 | --filter 'protect .well-known' \ 19 | --filter 'protect releases' \ 20 | --checksum \ 21 | _site/ \ 22 | ${DEST} 23 | 24 | -------------------------------------------------------------------------------- /_scripts/version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APP=`realpath .` 4 | 5 | pushd $APP/releases >/dev/null || { 6 | echo "Failed to enter release directory." 7 | exit 1 8 | } 9 | 10 | subdir=$1 11 | version=$2 12 | 13 | if [ "$subdir" == "" ] || [ "$version" == "" ]; then 14 | echo "Usage:" 15 | echo " $0 [.|extensions|desktop|mobile] " 16 | exit 1 17 | fi 18 | 19 | unversioned=`find $subdir -type f | grep -v '.gitignore' | egrep -v '[0-9]+\.[0-9]+'` 20 | 21 | for f in `echo "$unversioned"`; do 22 | file=`echo $f | sed -r 's|((\.[a-z0-9]{2,3}))+$||i'` 23 | ext=`echo $f | perl -pe 's|.*?((\.[a-z0-9]{2,3})+)|\1|i'` 24 | new="${file}-${version}${ext}" 25 | mv $f $new 26 | echo "Versioned: $new" 27 | done 28 | 29 | popd > /dev/null 30 | -------------------------------------------------------------------------------- /_scripts/watch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function do_make () { 4 | make \ 5 | | grep -v 'Nothing to be done' \ 6 | | grep -v '\(Entering\|Leaving\) directory' 7 | } 8 | 9 | function changes () { 10 | inotifywait.exe -r --exclude='(\.git|node_modules|_site)' -q . 11 | } 12 | 13 | do_make 14 | while changes; do 15 | do_make 16 | done 17 | 18 | -------------------------------------------------------------------------------- /contact.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Contact us" 4 | permalink: "contact/" 5 | --- 6 | 7 |
    8 |
    9 | 10 |
    11 |
    12 |
    13 |

    Turtl Community

    14 |

    15 | Join the Turtl Community! Ask questions, suggest features, and 16 | get help from the team or from other community members. This is 17 | the one place for discussion about Turtl! 18 |

    19 |
    20 |
    21 |
    22 |
    23 |

    Twitter

    24 |

    25 | We're on Twitter! Follow 26 | @turtlapp for updates, 27 | and @message or DM us if you have questions, suggestions, etc. 28 |

    29 |
    30 |
    31 |
    32 |
    33 |

    Github

    34 |

    35 | Check out our roadmap, upcoming features, and current bug reports 36 | on our Github. 37 |

    38 |
    39 |
    40 |
    41 |
    42 |

    Email

    43 |

    44 | For general enquiries (sales, legal, etc), please email info@turtlapp.com. 45 | Please keep in mind, this is not a support email. 46 |

    47 |
    48 |
    49 |
    50 |
    51 |
    52 | 53 | {% comment %} 54 |
    55 |
    56 | {% include mailing.html %} 57 |
    58 |
    59 | {% endcomment %} 60 | 61 | -------------------------------------------------------------------------------- /contributing/ecla.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Entity Contributor License Agreement" 4 | permalink: "contributing/ecla/" 5 | --- 6 | 7 | # Lyon Bros LLC Entity Contributor License Agreement 8 | 9 | Thank you for your interest in contributing to Lyon Bros LLC's Turtl project ("We" or "Us"). 10 | 11 | This contributor agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please sign it and send it to Us by electronic submission, following the instructions at https://turtlapp.com/contributing/. This is a legally binding document, so please read it carefully before agreeing to it. The Agreement may cover more than one software project managed by Us. 12 | 13 | ## 1. Definitions 14 | 15 | "You" means any Legal Entity on behalf of whom a Contribution has been received by Us. "Legal Entity" means an entity which is not a natural person. "Affiliates" means other Legal Entities that control, are controlled by, or under common control with that Legal Entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such Legal Entity, whether by contract or otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities which vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial ownership of such entity. 16 | 17 | "Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please follow the instructions in https://turtlapp.com/contributing/. 18 | 19 | "Copyright" means all rights protecting works of authorship owned or controlled by You or Your Affiliates, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You. 20 | 21 | "Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material. 22 | 23 | "Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 24 | 25 | "Submission Date" means the date on which You Submit a Contribution to Us. 26 | 27 | "Effective Date" means the date You execute this Agreement or the date You first Submit a Contribution to Us, whichever is earlier. 28 | 29 | 30 | ## 2. Grant of Rights 31 | 32 | ### 2.1 Copyright License 33 | 34 | (a) You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement. 35 | 36 | (b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3. 37 | 38 | 39 | ### 2.2 Patent License 40 | 41 | For patent claims including, without limitation, method, process, and apparatus claims which You or Your Affiliates own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. 42 | 43 | ### 2.3 Outbound License 44 | 45 | Based on the grant of rights in Sections 2.1 and 2.2, if We include Your Contribution in a Material, We may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. As a condition on the exercise of this right, We agree to also license the Contribution under the terms of the license or licenses which We are using for the Material on the Submission Date. 46 | 47 | ### 2.4 Moral Rights. 48 | 49 | If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect. 50 | 51 | ### 2.5 Our Rights. 52 | 53 | You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate. 54 | 55 | ### 2.6 Reservation of Rights. 56 | 57 | Any rights not expressly licensed under this section are expressly reserved by You. 58 | 59 | ## 3. Agreement 60 | 61 | You confirm that: 62 | 63 | (a) You have the legal authority to enter into this Agreement. 64 | 65 | (b) You or Your Affiliates own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2. 66 | 67 | (c) The grant of rights under Section 2 does not violate any grant of rights which You or Your Affiliates have made to third parties. 68 | 69 | (d) You have followed the instructions in https://turtlapp.com/contributing/, if You do not own the Copyright in the entire work of authorship Submitted. 70 | 71 | ## 4. Disclaimer 72 | 73 | EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW. 74 | 75 | ## 5. Consequential Damage Waiver 76 | 77 | TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED. 78 | 79 | ## 6. Miscellaneous 80 | 81 | 6.1 This Agreement will be governed by and construed in accordance with the laws of United States of America excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement. 82 | 83 | 6.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings. 84 | 85 | 6.3 If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement. 86 | 87 | 6.4 The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety. 88 | 89 | 6.5 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law. 90 | 91 | {% include signature.html sigtype="ecla" %} 92 | 93 | -------------------------------------------------------------------------------- /contributing/icla.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Individual Contributor License Agreement" 4 | permalink: "contributing/icla/" 5 | --- 6 | 7 | # Lyon Bros LLC Individual Contributor License Agreement 8 | 9 | Thank you for your interest in contributing to Lyon Bros LLC's "Turtl" project ("We" or "Us"). 10 | 11 | This contributor agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please sign it and send it to Us by electronic submission, following the instructions at https://turtlapp.com/contributing/. This is a legally binding document, so please read it carefully before agreeing to it. The Agreement may cover more than one software project managed by Us. 12 | 13 | ## 1. Definitions 14 | 15 | "You" means the individual who Submits a Contribution to Us. 16 | 17 | "Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please follow the instructions in https://turtlapp.com/contributing/. 18 | 19 | "Copyright" means all rights protecting works of authorship owned or controlled by You, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You. 20 | 21 | "Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material. 22 | 23 | "Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 24 | 25 | "Submission Date" means the date on which You Submit a Contribution to Us. 26 | 27 | "Effective Date" means the date You execute this Agreement or the date You first Submit a Contribution to Us, whichever is earlier. 28 | 29 | 30 | ## 2. Grant of Rights 31 | 32 | ### 2.1 Copyright License 33 | 34 | (a) You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement. 35 | 36 | (b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3. 37 | 38 | 39 | ### 2.2 Patent License 40 | 41 | For patent claims including, without limitation, method, process, and apparatus claims which You own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. 42 | 43 | ### 2.3 Outbound License 44 | 45 | Based on the grant of rights in Sections 2.1 and 2.2, if We include Your Contribution in a Material, We may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. As a condition on the exercise of this right, We agree to also license the Contribution under the terms of the license or licenses which We are using for the Material on the Submission Date. 46 | 47 | ### 2.4 Moral Rights. 48 | 49 | If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect. 50 | 51 | ### 2.5 Our Rights. 52 | 53 | You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate. 54 | 55 | ### 2.6 Reservation of Rights. 56 | 57 | Any rights not expressly licensed under this section are expressly reserved by You. 58 | 59 | ## 3. Agreement 60 | 61 | You confirm that: 62 | 63 | (a) You have the legal authority to enter into this Agreement. 64 | 65 | (b) You own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2. 66 | 67 | (c) The grant of rights under Section 2 does not violate any grant of rights which You have made to third parties, including Your employer. If You are an employee, You have had Your employer approve this Agreement or sign the Entity version of this document. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement. 68 | 69 | (d) You have followed the instructions in https://turtlapp.com/contributing/, if You do not own the Copyright in the entire work of authorship Submitted. 70 | 71 | ## 4. Disclaimer 72 | 73 | EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW. 74 | 75 | ## 5. Consequential Damage Waiver 76 | 77 | TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED. 78 | 79 | ## 6. Miscellaneous 80 | 81 | 6.1 This Agreement will be governed by and construed in accordance with the laws of United States of America excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement. 82 | 83 | 6.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings. 84 | 85 | 6.3 If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement. 86 | 87 | 6.4 The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety. 88 | 89 | 6.5 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law. 90 | 91 | {% include signature.html sigtype="icla" %} 92 | 93 | -------------------------------------------------------------------------------- /contributing/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Contributing to Turtl" 4 | permalink: "contributing/" 5 | --- 6 | 7 | # Contributing to the Turtl project 8 | {% include toc.md %} 9 | 10 | Thanks for your interest in helping build Turtl! Turtl is an open-source project 11 | owned and operated by [Lyon Bros LLC](https://lyonbros.com). 12 | 13 | ## Sign the Contributor License Agreement 14 | 15 | In order for us to accept your contributions to the Turtl project, you need to 16 | read and agree to the Contributor License Agreement. You can find the agreements 17 | here: 18 | 19 | - [Individual Contributor License Agreement](/contributing/icla) 20 | For individuals wanting to contribute 21 | - [Entity Contributor License Agreement](/contributing/ecla) 22 | If you are contributing code as a member of a company, organization, or other 23 | entity, have someone from your organization who makes legal decisions sign 24 | this form. 25 | 26 | ## Help us code 27 | 28 | Our central [Github issue tracker](https://github.com/turtl/tracker/issues) 29 | covers all of the Turtl projects. 30 | 31 | We have marked [**a list of items in our issue tracker you can help with**](https://github.com/turtl/tracker/issues?q=is%3Aissue+is%3Aopen+milestone%3A%2A+label%3Astatus%3Ahelp-wanted)! 32 | 33 | This includes all items with the `help-wanted` tag that are a part of one of 34 | our milestones. There are some issues that are very core to the app and 35 | require intimate knowledge of its inner workings, and these are generally not 36 | marked with `help-wanted`. 37 | 38 | Please ask before working on issues that do not have a milestone as these are 39 | generally meant to track ideas that haven't been fully realized or "maybe" 40 | features that we want to consider but haven't decided to include yet. 41 | 42 | Once again, *if you work on issues that are not part of a milestone, chances are 43 | your PR will be rejected*. When in doubt, pull issues off the [help wanted list](https://github.com/turtl/tracker/issues?q=is%3Aissue+is%3Aopen+milestone%3A%2A+label%3Astatus%3Ahelp-wanted)! 44 | 45 | ## Translations 46 | 47 | Would you like to translate Turtl to your language? Thanks for helping out! We 48 | love being multi-lingual. 49 | 50 | First, check if someone has already started translating in your language by 51 | looking through the [existing translations](https://github.com/turtl/js/tree/master/locales). 52 | 53 | If there's one for your language/locale already, feel free to make any updates 54 | to the file and submit a pull request. 55 | 56 | If you language/locale isn't there, copy the [latest language template file](https://github.com/turtl/js/blob/master/locales/locale.js.template) 57 | to a new file with the format `_.js` (if your 58 | language doesn't have a specific locale code, just use the language code again). 59 | For example, a Spanish translation in the Mexican locale would be `es_mx.js`. 60 | 61 | Now just fill in what you can and submit a pull request on Github! Keep in mind 62 | that translations are significant contributions, and therefor you must [sign the CLA](#sign-the-contributor-license-agreement) 63 | before new translations can be accepted. 64 | 65 | ## Project coding conventions 66 | 67 | Please review the programming conventions used for Turtl's various projects 68 | before you spend time writing code. 69 | 70 | ### Javascript 71 | 72 | These conventions apply to the following projects: 73 | 74 | - [js](https://github.com/turtl/js) 75 | - [android](https://github.com/turtl/android) 76 | - [desktop](https://github.com/turtl/desktop) 77 | - [browser-extension](https://github.com/turtl/browser-extension) 78 | 79 | Let's go over the basics. Note that some of these rules are more loosely 80 | enforced than others. If you make a significant contribution that works 81 | beautifully but you used `camelCasing` it's probably not going to be a big deal. 82 | 83 | - __Localized string literals__: Turtl is a multi-lingual application, and uses 84 | user-submitted translations. We build the localization template by parsing our 85 | own UI code. What this means is that we specifically look for `i18next.t("Delete")` 86 | in javascript and {% raw %}`{{t "Delete"}}`{% endraw %} in our templates, and 87 | we add the string "Delete" to the translation template. 88 | 89 | This means that you cannot put the string literals into variables! For 90 | instance: 91 | 92 | ```js 93 | // good 94 | var title = i18next.t('Edit note'); 95 | 96 | // good 97 | var title = action == 'add' ? i18next.t('Add note') : i18next.t('Edit note'); 98 | 99 | // bad! we won't be able to parse this 100 | var key = 'Edit note'; 101 | var title = i18next.t(key); 102 | 103 | // bad! we won't be able to parse this 104 | var title = i18next.t(action == 'add' ? 'Add note' : 'Edit note'); 105 | ``` 106 | 107 | In other words, when calling `i18next.t()` in javascript or {% raw %}`{{t ...}}`{% endraw %} 108 | in handlebars, *please only use literal strings*! 109 | 110 | - __Whitespace__: Please use readable whitespace. 111 | 112 | Examples: 113 | 114 | ```js 115 | // fine 116 | function test(arg1, arg2) { 117 | ... 118 | } 119 | 120 | // fine (dropped braces are OK) 121 | if(condition) 122 | { 123 | ... 124 | } 125 | 126 | // one-line functions are also fine 127 | var cb = function(err, res) { ... }; 128 | 129 | // nope. please use readable whitespace 130 | function test(arg1,arg2){ 131 | } 132 | 133 | // nope. use whitespace! 134 | var obj = { 135 | key:val1, 136 | key:val2, 137 | }; 138 | ``` 139 | 140 | - __Variable declarations__: `var`-per-declaration is the preferred method, but 141 | if this is against your normal style, that's ok. One thing that will not be 142 | tolerated is leading commas. 143 | 144 | Examples: 145 | 146 | ```js 147 | // good 148 | var my_data = get_some_data(); 149 | var success = send_some_data(data); 150 | 151 | // if you must. 152 | var my_data = get_some_data(), 153 | success = send_some_data(data); 154 | 155 | // Not allowed (leading commas, ugh) 156 | var my_data = get_some_data() 157 | , success = send_some_data(data); 158 | ``` 159 | - __Trailing commas__: Either leave them at the end of the line on the last item 160 | or take the trailing comma off. Do *NOT* put the comma before the item. 161 | 162 | Examples: 163 | 164 | ```js 165 | // good (trailing comma on last item is great A+++) 166 | var obj = { 167 | name: 'andrew', 168 | hates: 'leading commas', 169 | seriously: 'do not do it', 170 | }; 171 | 172 | // good (no trailing comma on last item also fine) 173 | var obj = { 174 | name: 'slappy', 175 | friends: 0 176 | }; 177 | 178 | // NO. never. 179 | var obj = { 180 | name: 'andrew' 181 | , hates: 'leading commas' 182 | , seriously: 'your code will be roundly rejected' 183 | }; 184 | ``` 185 | 186 | - __Underscores__: `use_underscores` instead of `camelCasing`. The exception is 187 | when defining top-level classes, which use `CapitalCamelCasing`. 188 | 189 | Example: 190 | 191 | ```js 192 | // good 193 | var user_settings = (new User()).get_settings(); 194 | 195 | // good (defining a top-level class) 196 | const User = Composer.Model.extend({...}); 197 | 198 | // nope 199 | var userSettings = ...; 200 | 201 | // nope 202 | function getAllNotes() ... 203 | ``` 204 | 205 | {% comment %} 206 | - __Tabs__: use `[tabs]` instead of `[spaces]`. If you are using Sublime Text, 207 | you are most likely not using tabs, even if you think you are. 208 | If you have to drop an `if` into multiple lines, do it like so: 209 | 210 | ```js 211 | // good 212 | if( 213 | condition1 && 214 | condition2 215 | ) { ... } 216 | 217 | // good 218 | if( condition1 && 219 | confition2 ) { ... } 220 | ``` 221 | 222 | where `condition1` would have a `[tab]` between it and the opening paren. 223 | {% endcomment %} 224 | 225 | #### Third-party libraries 226 | 227 | Turtl strives to use *as little third-party code as possible* in its front-end 228 | clients. The reason for this is that each library that *is* included has to be 229 | vetted for possible security leaks. 230 | 231 | For this reason, if you do feel a third-party library would suit the project, 232 | please note this in your pull request. Put your third-party libraries directly 233 | into the source tree and version them. If the third-party library makes any 234 | kind of AJAX calls, form posts, writes any scripts to <head>, or makes 235 | any other outbound connection, there is a very good chance your changes will not 236 | be merged. 237 | 238 | Dependency management tools like bower/npm/etc are not to be used or included 239 | in the javascript-based projects. 240 | Note that we do use npm to power some aspects of the build system (lessc, 241 | handlebars, postcss, etc) but under no cirumcstances does it download code to 242 | be included in the app itself. 243 | 244 | Please note that third-party libraries included in Turtl cannot be licensed 245 | copyleft (eg, GPLv3). This prevents us from relicensing Turtl, for instance 246 | if we want to release an app in the Apple iOS store. MIT/BSD 247 | licenses are strongly favored, but others will be considered on a case-by-case 248 | basis. 249 | 250 | Please make sure any third-party code you include contains the license it uses 251 | in its source file(s). 252 | 253 | ### Rust 254 | 255 | Our Rust code follows the standard Rust conventions, but I'll list some of the 256 | bigger ones here: 257 | 258 | - __Indent with two spaces__: Please follow this. All of our code uses two-space 259 | indendation and if you use tabs or four spaces or anything else, you will be 260 | asked to re-tab. 261 | 262 | - __Use underscores__: No camelCase. This is a hard rule. 263 | 264 | Examples: 265 | 266 | ```rust 267 | // good 268 | fn get_data() -> u32 { 269 | 0 270 | } 271 | 272 | // bad 273 | fn thisIsNotJava(orCSharp: u32) -> { ... } 274 | ``` 275 | 276 | ## Questions 277 | 278 | If you have questions on any of the above, or run into a situtation that isn't 279 | covered, please [reach out to us](/contact)! 280 | 281 | -------------------------------------------------------------------------------- /contributing/sign-error.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "There was an error processing your signature" 4 | permalink: "contributing/sign-error/" 5 | --- 6 | 7 | # There was an error processing your signature 8 | 9 | Sorry, but we ran into a problem processing your electronic signature. Please 10 | contact us at [info@turtlapp.com](mailto:info@turtlapp.com), or hit the back button and 11 | try to submit the form again. 12 | 13 | -------------------------------------------------------------------------------- /contributing/sign-thanks.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Thanks for signing!" 4 | permalink: "contributing/sign-thanks/" 5 | --- 6 | 7 | # Thanks for signing! 8 | 9 | We will be notified of your signature within a few minutes. We'll do a quick 10 | review, and if everything checks out we should be able to start accepting your 11 | contributions to the Turtl project. 12 | 13 | Happy hacking =] 14 | 15 | - [Back to the Turtl homepage](/) 16 | - [Back to the contributing page](/contributing) 17 | - [Go to the Turtl Github »](https://github.com/turtl) 18 | 19 | -------------------------------------------------------------------------------- /css/main.less: -------------------------------------------------------------------------------- 1 | @import '../node_modules/uikit/src/less/uikit.theme.less'; 2 | 3 | // ----------------------------------------------------------------------------- 4 | // vars 5 | // ----------------------------------------------------------------------------- 6 | @font-accent: "Noto serif", "Times New Roman", Times, serif; 7 | @theme-background: #ededed; 8 | @font-color: #333; 9 | 10 | // ----------------------------------------------------------------------------- 11 | // some uikit overrides 12 | // ----------------------------------------------------------------------------- 13 | @global-font-family: 'Lato', sans-serif; 14 | @global-primary-background: #83b362; 15 | @breadcrumb-divider-margin-horizontal: 10px; 16 | @base-em-color: inherit; 17 | @inverse-global-color: fade(@global-inverse-color, 85%); 18 | 19 | // ----------------------------------------------------------------------------- 20 | // base styles 21 | // ----------------------------------------------------------------------------- 22 | html {color: @font-color;} 23 | body {background: @theme-background;} 24 | 25 | @font-face {font-family: 'Fontello'; src: url('/images/font/fontello.woff2');} 26 | icon {font-family: 'Fontello';} 27 | 28 | a, .uk-link {color: saturate(darken(@global-primary-background, 8%), 10%);} 29 | 30 | #container {min-height: 500px;} 31 | .main-nav-container {background: #fff; box-shadow: 0 0 12px 1px rgba(0, 0, 0, 0.07);} 32 | 33 | nav.main { 34 | .uk-logo {color: #000; font-size: 32px; font-family: @font-accent;} 35 | .uk-logo { 36 | img {max-width: 1.4em; max-height: 1.4em; margin-right: 0.46em;} 37 | sup {font-size: 12px; transition: transform 0.3s;} 38 | &:hover sup {transform: translate(0, -3px);} 39 | } 40 | ul.uk-navbar-nav {flex-wrap: wrap;} 41 | } 42 | 43 | footer { 44 | ul {list-style: none; margin: 0; padding: 0;} 45 | } 46 | 47 | body.home { 48 | .hero {position: relative; background-image: url(/images/home/notebook.jpg);} 49 | .hero { 50 | .overlay {position: absolute; z-index: 0; top: 0; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.7);} 51 | .hero-container {position: relative;} 52 | .info { 53 | h1 {font-family: 'Slabo 13px', serif; font-weight: bold; color: #fff;} 54 | @media only screen and (min-width: @breakpoint-medium) { 55 | & {padding-left: 60px;} 56 | } 57 | } 58 | .devices { 59 | img {max-height: 400px;} 60 | } 61 | } 62 | 63 | .turtl-cover { 64 | &:before {content: ' '; position: absolute; top: 0; bottom: 0; left: 0; right: 0; background: #000;} 65 | &.turtl-cover-light { 66 | &:before {background: #fff;} 67 | .uk-cover {opacity: 0.2;} 68 | } 69 | .uk-cover {opacity: 0.3;} 70 | p {font-size: 1.2em;} 71 | } 72 | 73 | .more-features {font-size: 1.2em;} 74 | } 75 | 76 | .feature-screenshots { 77 | img {box-shadow: 0 3px 8px 1px rgba(0, 0, 0, 0.2);} 78 | } 79 | 80 | .download { 81 | .uk-button {width: 100%;} 82 | .uk-button { 83 | icon {margin-right: 4px; font-size: 1.2em;} 84 | } 85 | } 86 | 87 | .documentation { 88 | .uk-breadcrumb {border-left: 3px solid @global-primary-background;} 89 | } 90 | 91 | .premium { 92 | @premium-padding: 12px; 93 | @media (min-width: @breakpoint-small) { 94 | .premium-card {position: relative; top: -@premium-padding; cursor: pointer;} 95 | .premium-card .card-inner {padding-top: @premium-padding; padding-bottom: @premium-padding;} 96 | } 97 | } 98 | 99 | .donate { 100 | code {max-width: 100%; word-wrap: break-word; white-space: pre-wrap;} 101 | } 102 | 103 | @button-accent-bg: #904890; 104 | .button-accent {color: #fff !important; background-color: @button-accent-bg !important; transition: background-color 0.3s;} 105 | .button-accent:hover {background-color: darken(@button-accent-bg, 5%) !important;} 106 | 107 | // CLA. could prob use uikit but just want to get this out the door 108 | .signature {} 109 | .signature { 110 | table {} 111 | table { 112 | tr.website {position: absolute; right: 2000%;} 113 | td { 114 | &:first-child {width: 25%; padding: 0 4px 0 0; font-weight: bold;} 115 | em {position: absolute; margin: 0 0 0 -10px; display: inline-block; padding: 0 2px; color: red;} 116 | input[type=text], 117 | input[type=email], 118 | select { 119 | & {margin: 0 0 4px 0; padding: 2px; background: #fff; border: 1px solid #777;} 120 | &.error-field {border-bottom: 1px solid red;} 121 | } 122 | } 123 | } 124 | ul.error {margin: 16px 0; padding: 12px; list-style: none; color: #f44; background: #222;} 125 | ul.error { 126 | &.hide {display: none;} 127 | } 128 | } 129 | 130 | .announcement {} 131 | .announcement { 132 | h1 {margin: 0; padding: 0; font-size: 24px;} 133 | a {display: block; padding: 12px 0; color: #fff; text-align: center; text-decoration: none; background: #8FC46A; transition: background-color 0.3s;} 134 | a:hover {background-color: darken(#8FC46A, 5%);} 135 | } 136 | 137 | credit {display: block; .uk-text-small(); .uk-text-center();} 138 | 139 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: App architecture | Documentation 3 | layout: documentation 4 | permalink: 'docs/architecture/' 5 | --- 6 | 7 | # App architecture 8 | {% include toc.md %} 9 | 10 | 14 | 15 | Let's go over the basics of how Turtl works. 16 | 17 | ## Sharing 18 | 19 | Sharing allows you to collaborate on a board with someone else. The general 20 | idea is this: each note has a unique key that decrypts it. This key is 21 | encrypted with the key of any space the note is in, and this encrypted key is 22 | stored in the note's data. So if you have an encrypted note, and you have the 23 | key of a space that note is in, you can decrypt the note. Giving each object 24 | in Turtl a unique key allows sharing only specific data with any set of people 25 | without compromising the master key of your account. 26 | 27 | Sharing is done only on a space basis. You cannot share single notes. You can, 28 | however, put a single note into a space and share that space. 29 | 30 | ## Syncing 31 | 32 | Whenever you change data in your profile, the changed objects are encrypted, 33 | saved to local storage, and the syncing system is notified of the 34 | change. All changed objects are saved in a local syncing table, which the 35 | sync system periodically reads. If it finds changes, it sends them up to the 36 | Turtl server, in order, using a bulk send (all items are sent at once). 37 | 38 | At the same time, the sync system polls the API for incoming changes to 39 | your profile. This can happen when you use Turtl on another device, or if a 40 | shared space you're a member of has changes on it. Then the reverse happens: 41 | the changes are pulled down, saved (encrypted) to the local storage, and the 42 | app is notified of the changes (at which point it loads the changes into 43 | memory, decrypted, if it needs to). 44 | 45 | If at any point Turtl gets disconnected, it holds outgoing sync data 46 | indefinitely until it gets a connection. What this means is that after you log 47 | in (which requires a connection), you can operate completely in offline mode 48 | until you are connected again at which point all the changes you've made will 49 | be synced. 50 | 51 | Interested in how this all works in detail? [Read more about Turtl's syncing 52 | system »](/docs/syncing) 53 | 54 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Documentation 3 | layout: documentation 4 | permalink: 'docs/' 5 | --- 6 | 7 | # Turtl Documentation 8 | 9 | Welcome to Turtl's docs. If you want to learn about how Turtl works, either high 10 | level or gritty details, then you've come to the right place! 11 | 12 |
    13 |
    14 |
    15 | 16 | ### [Security and encryption](/docs/security) 17 | Learn about how Turtl uses encryption to protect your data. 18 | 19 | - [Encryption explained](/docs/security#encryption-explained) 20 | - [Encryption specifics](/docs/security#encryption-specifics) 21 | - [Stay logged in](/docs/security#stay-logged-in) 22 | - [When is Turtl *not* secure?](/docs/security#when-is-turtl-not-secure) 23 | 24 |
    25 |
    26 |
    27 |
    28 | 29 | ### [App architecture](/docs/architecture) 30 | Read about the ideas behind Turtl and how it's put together. 31 | 32 | - [Sharing](/docs/architecture#sharing) 33 | - [Syncing](/docs/architecture#syncing) 34 | 35 |
    36 |
    37 |
    38 | 39 | ## Turtl's server 40 | 41 | Want to run Turtl internally? Head over to the [section about running 42 | your own Turtl server](https://github.com/turtl/server#running-the-server). 43 | 44 | -------------------------------------------------------------------------------- /docs/security/encryption-specifics.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: 'Encryption specifics | Documentation' 4 | permalink: 'docs/security/encryption-specifics/' 5 | --- 6 | 7 | # Encryption specifics 8 | {% include toc.md %} 9 | 10 | 15 | 16 | This page will explain the specific algorithms and methods Turtl uses to encrypt, 17 | decrypt, (de)serialize, and authenticate data. 18 | 19 | ## Subkeys and sharing 20 | 21 | Every encrypted object in Turtl has its own key. This goes for spaces, boards, 22 | and notes. This means that every object can be decrypted independently of the 23 | profile's master key (generated from the user's authentication info). 24 | 25 | Encrypted objects have the ability to store their own key in their data, 26 | encrypted via another object's key. Sounds confusing, so the primary example 27 | would be this: Note A has its own key that will decrypt its data. Note A is in 28 | Space B. Note A's data contains Note A's key encrypted with Space B's key. So 29 | if Alice shares Space B with Bob, she can share Space B's key, and now Bob has 30 | the ability to decrypt any note in Space B (including Note A). 31 | 32 | This is what allows objects to be sharable in Turtl without compromising the 33 | master key...sharing can be done granularly and on a per-object basis. That said 34 | the only objects that are currently sharable in Turtl are spaces. 35 | 36 | ## Algorithms 37 | 38 | As of v0.7.0, Turtl now handles all encryption in [the Turtl core](https://github.com/turtl/core-rs), 39 | which uses [libsodium](https://download.libsodium.org/doc/) under the hood for 40 | all cryptographic operation. 41 | 42 | ### Key generation 43 | 44 | When you log in, Turtl takes your email (as a salt) and your password (as the 45 | main input) and runs them through libsodium's default key-derivation function 46 | (scryptsalsa208sha256). 47 | 48 | The resulting key is your account's master key. 49 | 50 | ### Ciphers and modes 51 | 52 | Turtl uses [libsodium](https://download.libsodium.org/doc/) for all low 53 | level encryption. Symmetric encryption uses the chacha20poly1305 (IETF) 54 | algorithm, and asymmetric crypto uses the more abstract [libsodium sealed box](https://download.libsodium.org/doc/public-key_cryptography/sealed_boxes.html). 55 | 56 | ### Symmetric Serialization format 57 | Turtl packs encrypted data in the following binary format: 58 | 59 | ~~~ 60 | |-2 bytes-| |-1 byte----| |-N bytes-----------| |-1 byte-----| |-N bytes-| |-N bytes--| 61 | | version | |desc length| |payload description| |nonce length| | nonce | |ciphertext| 62 | ~~~ 63 | 64 | 1. __Version__ - The serialization version. Lets tcrypt know how this data was 65 | serialized and how to parse it. 66 | 1. __Description length__ - How many bytes long the payload description is. 67 | 1. __Payload description__ - Describes the cipher used in this payload. 68 | 1. __Nonce__ - The nonce this payload uses. 69 | 1. __Ciphertext__ - Our actual encrypted data, in binary form. 70 | 71 | ### Authentication 72 | 73 | The chacha20poly1305 algorithm handles payload authentication internally, which 74 | means that the `ciphertext` itself has a tag used to verify that it has not been 75 | modified in any way. 76 | 77 | ### Asymmetric serialization format 78 | 79 | Data encrypted with the sealed box algorithm is in the following format: 80 | ~~~ 81 | |-1 byte--| |-N bytes--| 82 | | version | |ciphertext| 83 | ~~~ 84 | 85 | Note that because the sealed box handles the serialization for us, our own 86 | format is painfully simple, and just includes a version that lets us switch 87 | algorithms/formats later on if needed. 88 | 89 | -------------------------------------------------------------------------------- /docs/security/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Security and encryption | Documentation 3 | layout: documentation 4 | permalink: 'docs/security/' 5 | --- 6 | 7 | # Security and encryption 8 | {% include toc.md %} 9 | 10 | 14 | 15 | 16 | Turtl uses encryption to protect your data in such a way that only you, and 17 | those you choose, are able to view your data. Keep reading for a high-level 18 | overview of Turt's encryption and how it protects you. 19 | 20 | ## Encryption explained 21 | 22 | Simply put, encryption is the process of scrambling data. Generally, this is 23 | done using a "key" which is usually a passphrase. The only way to de-scramble 24 | the data is using that passphrase. 25 | 26 | Turtl's encryption works by generating a key for you based on your 27 | email and password. This key is used to lock and unlock (or encrypt and 28 | decrypt) your data and keep it private. All of the encryption in Turtl happens 29 | before any data leaves the app, meaning that even if someone is snooping in on 30 | your connection or someone hacks our database, everything you've put into Turtl 31 | is just gibberish to them. 32 | 33 | Without the keys that only you hold, your data is useless. 34 | 35 | ### Keys and sharing 36 | 37 | As mentioned, Turtl creates a key for you when you log in based on your email 38 | and password. It wouldn't be very useful if you had to give people this key when 39 | you shared data with them because it would give them access to all your data. 40 | Instead, Turtl generates a *new, random* key for each object. This key is what 41 | is sent to people when sharing, allowing them to unlock the specific item you 42 | send them and nothing else. 43 | 44 | Keys are stored one of two ways: 45 | 46 | 1. __Spaces__. Space keys are stored in your "keychain" which is a collection of 47 | keys stored with your profile. These keys are all encrypted using your master 48 | key so *only you* are able to read your keychain. 49 | 1. __Notes/boards__. Notes and boards store their key in their own data, encrypted with the key of 50 | the space they belong to. What this means is that once a user has access to a 51 | space (and the space's key) they can also decrypt all the notes and boards in that space. 52 | This allows sharing of entire spaces *without* having to share the key of each 53 | note in that space. 54 | 55 | ## Encryption specifics 56 | 57 | If you're looking for a more comprehensive look at how Turtl does encryption, 58 | [check out the encryption specifics page](/docs/security/encryption-specifics) of the docs 59 | which goes over the ciphers, block modes, and other methods Turtl uses when 60 | handling your data. 61 | 62 | [Encryption specifics »](/docs/security/encryption-specifics) 63 | 64 | ## Stay logged in 65 | 66 | Turtl has a feature that keeps you logged in if the app is closed and reopened. 67 | This feature may have security implications. [Read more about the "Stay logged in" 68 | feature](/docs/security/stay-logged-in). 69 | 70 | ## When is Turtl *not* secure? 71 | 72 | Here are some possible scenarios where Turtl's security measures will fail you. 73 | We try to provide an exhaustive list so you're aware of the dangers of relying 74 | on Turtl. 75 | 76 | - __When we make mistakes__. That's right, we're human. It's entirely possible 77 | that bugs in the Turtl client leave your data exposed, *especially* at our 78 | early stage. 79 | - __When you use a bad password__. Turtl encrypts just about everything before 80 | sending it to the server. It does this using a cryptographic key based off of 81 | your email and password. If you choose a password that's 82 | short, predictable, easy to guess, etc then your data is *not safe*. Choose a 83 | good password. Turtl has no restrictions on password length, we suggest 84 | you take advantage of this. 85 | - __When you invite someone to a space over email.__ Turtl has a feature that 86 | allows you to invite someone to share one of your spaces via email. Before 87 | sending, you are able to set a shared secret for the invite, which makes the 88 | invite useless unless the invitee enters the secret when they accept the invite 89 | (this secret must be communicated to the invitee separately). Without setting 90 | this secret, anybody who intercepts the invite email *can gain full access to 91 | the space and its data*. If you want to share something but need it to be 92 | secure, set the secret and communicate it (via phone, text message, etc) to the 93 | person you're inviting. Please note that the shared-secret method is not as 94 | secure as asymmetric encryption (used when inviting an *existing* user to a 95 | space), but it's a lot better than not having the shared secret at all (if you 96 | care about privacy). 97 | - __When someone you shared data with is compromised.__ If you share 98 | notes, spaces, or any other data with other Turtl users, you are giving them the 99 | ability to decrypt those pieces of your data. It's possible that the person you 100 | shared with isn't who you think they are, or they have a gun to their head and 101 | have no choice but to expose your data. Be very careful about who you share 102 | sensitive data with. 103 | - __When you have malware installed__. When you're logged in to Turtl, all your 104 | unencrypted data is sitting in your computer's memory. It's possible that a 105 | malicious program could gain access to the app's memory and read your data. 106 | Note that most operating systems have protections against this, but that doesn't 107 | make it impossible. 108 | - __When your operating system is compromised__. Although this may sound far 109 | fetched, it's possible that your entire operating system itself is maliciously 110 | programmed to send contents of memory from certain programs to certain corporate 111 | headquarters or government agencies. If you really want to elminate this 112 | possibility, use an open-source operating system (such as Linux or BSD). 113 | - __When your hardware is compromised__. It's not outside the realm of 114 | possibility that your computer's hardware is maliciously sharing the contents of 115 | your memory to a third party. 116 | - __When someone is holding a gun to your head__. Sometimes the easiest way to 117 | get your data is to threaten you or your family. Turtl has no countermeasures to 118 | protect against this, and it's up to you to make your own decisions. 119 | 120 | -------------------------------------------------------------------------------- /docs/security/stay-logged-in.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: 'Stay logged in | Documentation' 4 | permalink: 'docs/security/stay-logged-in/' 5 | --- 6 | 7 | # Stay logged in 8 | {% include toc.md %} 9 | 10 | 15 | 16 | The Turtl app has a feature called "Stay logged in" that remembers your previous 17 | login and logs you in automatically when opening the app. This feature trades 18 | security for convenience for people with longer passwords who don't want to log 19 | in by hand each time. 20 | 21 | Stay logged in uses the concept of login tokens: an object that stores the user's 22 | id, master key, auth token, and username. This is essentially a "key" to log in. 23 | It does not include the user's password, but it can be used to log in to the 24 | user's account. 25 | 26 | Below are some of the platform-specific ways that "Stay logged in" works. 27 | 28 | ## Desktop 29 | 30 | On desktop, when the user logs in (and has "Stay logged in" checked) the user's 31 | login token is encrypted and saved to disk. The key that encrypts the login 32 | token is handed back to the desktop app, and it stores this in a simple on-disk 33 | storage called "localStorage" which is stored in plaintext on the disk. 34 | 35 | This means that anybody with read access to the user's home directory (the 36 | default location of the app's files) can access the key that decrypts the user's 37 | login token and can gain access to their account. 38 | 39 | This risk can be mitigated by using drive encryption, however any malicious 40 | program that is able to run under your user will be able to steal your login 41 | token. 42 | 43 | If you value the security of your account, **it is recommended *not* to use 44 | "Stay logged in" on desktop!** 45 | 46 | ## Android 47 | 48 | On Android, when the user logs in (and has "Stay logged in" checked) the user's 49 | login token is encrypted and saved to disk. The key that encrypts the login 50 | token is handed back to the Android app, and it encrypts this key using the 51 | [Android Keystore](https://developer.android.com/training/articles/keystore) 52 | and stores the encrypted key into the app's preferences. 53 | 54 | This means that in order to decrypt the key for the login token, an attacker 55 | would need root access to the Android phone. 56 | 57 | It is recommended that you encrypt your device and *not* root your phone if 58 | using "Stay logged in" on Android. Also, it is safer if you use a device that 59 | has TEE/TPM hardware, which more actively protects the Keystore from leaking 60 | keys. 61 | 62 | -------------------------------------------------------------------------------- /docs/syncing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Syncing | Documentation' 3 | layout: documentation 4 | permalink: 'docs/syncing/' 5 | --- 6 | 7 | # Syncing 8 | {% include toc.md %} 9 | 10 | 15 | 16 | 17 | Let's dive into what makes Turtl's syncing system work. 18 | 19 | ## Client IDs (or the "cid") 20 | 21 | Each object having a globally unique ID that can be client-generated makes 22 | syncing painless. We do this using a few methods, some of which are actually 23 | borrowed from [MongoDB's Object ID schema](http://docs.mongodb.org/manual/reference/object-id/). 24 | 25 | Every client that runs the Turtl app creates and saves a client hash if it 26 | doesn't have one. This is the hex representation of 32 bytes of random data. 27 | 28 | The client id is formatted like this: 29 | 30 | ~~~ 31 | 12 bytes hex timestamp | 64 bytes client hash | 4 bytes hex counter 32 | ~~~ 33 | 34 | For example, the cid 35 | 36 | ~~~ 37 | 014edc2d6580b57a77385cbd40673483b27964658af1204fcf3b7b859adfcb90f8b8955215970012 38 | ~~~ 39 | 40 | breaks down as: 41 | 42 | ~~~ 43 | timestamp client hash counter 44 | ------------|----------------------------------------------------------------|-------- 45 | 014edc2d6580 b57a77385cbd40673483b27964658af1204fcf3b7b859adfcb90f8b895521597 0012 46 | | | | 47 | |- 1438213039488 |- unique hash |- 18 48 | ~~~ 49 | 50 | The timestamp is a unix millisecond timestamp value (with leading 0s to support 51 | longer times eventually). The client hash we already went over, and the counter 52 | is a value tracked in-memory that increments each time a cid is generated. The 53 | counter has a max value of 65535, meaning that the only way a client can produce 54 | a duplicate cid is by creating 65,535,001 objects in one second. We have some 55 | devoted users, but even for them creating 65M notes in a second would be 56 | difficult. 57 | 58 | So, the timestamp, client hash, and counter ensure that each cid created is 59 | unique not just to the client, but globally within the app as well (unless two 60 | clients create the same client hash somehow, but this is implausible). 61 | 62 | What this means is that we can create objects endlessly in any client, each with 63 | a unique cid, use those cids as primary keys in our database, and never have a 64 | collision. 65 | 66 | This is important because we can create data in the client, and not need server 67 | intervention or creation of IDs. A client can be offline for two weeks and then 68 | sync all of its changes the next time it connects without problems and without 69 | needing a server to validate its object's IDs. 70 | 71 | Using this scheme for generating client-side IDs has not only made offline mode 72 | possible, but has greatly simplified the syncing codebase in general. Also, 73 | having a timestamp at the beginning of the cid makes it sortable by order of 74 | creation, a nice perk. 75 | 76 | ## Queuing and bulk syncing 77 | 78 | Let's say you add a note in Turtl. First, the note data is encrypted 79 | (serialized). The result of that encryption is shoved into the local DB 80 | (IndexedDB) *and* the encrypted note data is also saved into an outgoing sync 81 | table (also IndexedDB). The sync system is alerted "hey, there are outgoing 82 | changes in the sync table" and if, after a short period, no more outgoing sync 83 | events are triggered, the sync system takes *all* pending outgoing sync records 84 | and sends them to a bulk sync API endpoint (in order). 85 | 86 | The API processes each one, going down the list of items and updating the 87 | changed data. It's important to note that Turtl doesn't support deltas! It only 88 | passes full objects, and replaces those objects when any one piece has changed. 89 | 90 | For each successful outgoing sync item that the API processes, it returns a 91 | success entry in the response, with the corresponding *local* outgoing sync ID 92 | (which was passed in). This allows the client to say "this one succeeded, remove 93 | it from the outgoing sync table" on a granular basis, retrying entries that 94 | failed automatically on the next outgoing sync. 95 | 96 | Here's an example of a sync sent to the API: 97 | 98 | ~~~ 99 | [ 100 | {id: 3, type: 'note', action: 'add', data: { }} 101 | ] 102 | ~~~ 103 | 104 | and a response: 105 | 106 | ~~~ 107 | { 108 | success: [ 109 | {id: 3, sync_ids: ['5c219', '5c218']} 110 | ] 111 | } 112 | ~~~ 113 | 114 | We can see that sync item "3" was successfully updated in the API, which allows 115 | us to remove that entry from our local outgoing sync table. The API also returns 116 | server-side generate sync IDs for the records it creates in its syncing log. We 117 | use these IDs passed back to *ignore* incoming changes from the API when incoming 118 | syncs come in later so we don't double-apply data changes. 119 | 120 | ### Why not use deltas? 121 | 122 | Wouldn't it be better to pass diffs/deltas around than full objects? If two 123 | people edit the same note in a shared space at the same time, then the 124 | last-write-wins architecture would overwrite data! 125 | 126 | Yes, diffs would be wonderful. However, consider this: at some point, an object 127 | would be an original, and a set of diffs. It would have to be collapsed back 128 | into the main object, and because the main object *and* the diffs would be 129 | client-encrypted, the server has no way of doing this. 130 | 131 | What this means is that the clients would not only have to sync notes/boards/etc 132 | but also the diffs for all those objects, and collapse the diffs into the main 133 | object then *save the full object back to the server*. 134 | 135 | To be clear, this is entirely possible. However, I'd much rather get the 136 | whole-object syncing working perfectly before adding additional complexity of 137 | diff collapsing as well. 138 | 139 | ## Polling for changes 140 | 141 | Whenever data changes in the API, a log entry is created in the API's "sync" 142 | table, describing what was changed and who it affects. This is also the place 143 | where, in the future, we might store diffs/deltas for changes. 144 | 145 | When the client asks for changes, at does so using a sequential ID, saying "hey, 146 | get me everything affecting my profile that happened after <last sync id>". 147 | 148 | The client uses long-polling to check for incoming changes (either to one's own 149 | profile or to shared resources). This means that the API call used holds the 150 | connection open until either a) a certain amount of time passes or b) new sync 151 | records come in. 152 | 153 | For each sync record that comes in, it's linked against the actual data stored 154 | in the corresponding table (so a sync record describing an edited note will pull 155 | out that note, in its current form, from the "notes" table). Each sync record is 156 | then handed back to the client, in order of occurence, so it can be applied to 157 | the local profile. 158 | 159 | The result is that changes to a local profile are applied to all connected 160 | clients within a few seconds. This also works for shared spaces, which are 161 | included in the sync record searches when polling for changes. 162 | 163 | ## File handling 164 | 165 | Files are synced separately from everything else. This is mainly because they 166 | can't just be shoved into the incoming/outgoing sync records due to their 167 | potential size. 168 | 169 | Instead, the following happens: 170 | 171 | ### Outgoing syncs (client -> API) 172 | 173 | TODO: update for v0.7 (core). 174 | 175 | ### Incoming syncs (API -> client) 176 | 177 | TODO: update for v0.7 (core). 178 | 179 | ## What's not in offline mode? 180 | 181 | All actions work in offline mode, except for a few that require server approval: 182 | 183 | - login (requires checking your auth against the API's auth database) 184 | - joining (creating an account) 185 | - changing your password 186 | - sharing a space 187 | - deleting your account 188 | 189 | -------------------------------------------------------------------------------- /docs/troubleshooting/logging-in.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: 'Troubleshooting: Logging in | Documentation' 4 | permalink: 'docs/troubleshooting/logging-in/' 5 | --- 6 | 7 | # Troubleshooting: Logging in 8 | {% include toc.md %} 9 | 10 | 14 | 15 | There are a number of problems that can keep you from logging into your account. 16 | 17 | ## Custom servers 18 | 19 | When upgrading to Turtl v0.7, your "Saved servers" are lost. This is an 20 | unfortunate consequence of the upgrade, and means you'll have to set up the 21 | servers you use again. 22 | 23 | ### Framanotes 24 | 25 | The most common non-default server is Framanotes. Please try [connecting to Framanotes](https://framanotes.org/) 26 | if you are having problems logging in! 27 | 28 | ## Forgot password 29 | 30 | If you forgot your password, the best we can do is remove the account and let 31 | you sign up again. We could give you direct access, but without knowing the 32 | password you wouldn't be able to decrypt any of your data anyway. 33 | 34 | Once again, we cannot reset your password. 35 | 36 | Please use a password manager. 37 | 38 | -------------------------------------------------------------------------------- /donate-thanks.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Donate to the Turtl project" 4 | permalink: "donate/thanks/" 5 | body_class: 'donate-thanks' 6 | --- 7 | 8 | Thank you! 9 | ========== 10 | 11 | Your contribution to the Turtl project is appreciated. Your donation not only 12 | helps Turtl's ongoing development, but sends a clear message: *Privacy matters.* 13 | 14 | Please consider following us on [Twitter](https://twitter.com/turtlapp) or on 15 | [Google+](https://plus.google.com/communities/105901682343154191710) to stay in 16 | touch with the team and get the latest news on features and releases. 17 | 18 | __Thanks again!__ 19 | 20 | \- The Turtl team 21 | 22 | -------------------------------------------------------------------------------- /donate.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Donate to the Turtl project" 4 | permalink: "donate/" 5 | body_class: 'donate' 6 | --- 7 | 8 |

    Donate to the Turtl project

    9 | 10 |

    11 | Until we are able to support ourselves by providing a Turtl Premium service, any 12 | and all donations help further continuing development. If you like 13 | using Turtl, please consider helping us make it better by giving anything you 14 | can. Thank you! 15 |

    16 | 17 |
    18 |
    19 |
    20 |

    Paypal

    21 | 29 |
    30 |
    31 |
    32 |
    33 |

    Bitcoin

    34 |
    1FXpcXhdiurerBjLm47YE6PemMxdFtcQt1
    35 |
    36 |
    37 |
    38 |
    39 |

    Ethereum

    40 |
    0x8738DaE708A23845f686B586cAcAC9C30f17b0dA
    41 |
    42 |
    43 |
    44 | 45 |
    46 | Donate a tweet 47 |
    48 | 49 | -------------------------------------------------------------------------------- /download.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Download Turtl" 4 | permalink: "download/" 5 | --- 6 | 7 |
    8 |
    9 | 10 |

    Download Turtl

    11 |

    12 | The desktop and mobile apps give you access to your data from any 13 | device. 14 |

    15 | {% include download.html %} 16 | 17 |

    Bookmarking extensions

    18 | 19 |

    20 | The bookmarking extensions put a button in your browser that makes it easy to 21 | bookmark the current page you're on. This is great for *quickly* saving sites 22 | to read later or for creating a list of pages you frequently visit. 23 |

    24 | 25 |
    26 | {% include extensions.html %} 27 |
    28 |
    29 |
    30 | 31 |
    32 |
    33 |

    Verification of downloaded files

    34 |

    35 | Want to verify your Turtl download? Follow these instructions: 36 |

    37 |
      38 |
    1. 39 | Import our public signing key into your GPG (key signature DEDF113E54248344163716B55C66FAD13222D757). 40 | 41 |
      $ wget https://keybase.io/orthecreedence/pgp_keys.asc?fingerprint=dedf113e54248344163716b55c66fad13222d757 -O turtl.asc
      42 | $ gpg --import turtl.asc
      43 |
    2. 44 |
    3. 45 | Grab the SHA256 signature file and signature file for the platform you want: 46 | 60 |
    4. 61 |
    5. 62 | Verify the GPG signature of the SHA256 file. 63 | 64 |
      $ gpg --verify desktop.sha256sums.sig desktop.sha256sums
      65 | gpg: Signature made Tue, Jul 24, 2018  9:12:17 PM CDT
      66 | gpg:                using RSA key 57CE4D9CD8D276B4
      67 | gpg: Good signature from "Andrew Lyon "
      68 | Primary key fingerprint: DEDF 113E 5424 8344 1637  16B5 5C66 FAD1 3222 D757
      69 |      Subkey fingerprint: B25B DF8F 8BB7 7454 ACFF  BA84 57CE 4D9C D8D2 76B4
      70 |
    6. 71 |
    7. 72 | If verification succeeded, now you can compare the SHA256 sum of the download you made to that file's entry in the SHA256 file: 73 | 74 |
      $ wget https://github.com/turtl/desktop/releases/download/v{{site.releases.desktop}}/turtl-{{site.releases.desktop}}-windows64.msi
      75 | $ sha256sum turtl-{{site.releases.desktop}}-windows64.msi
      76 | fb57740d84e6838d83035949477c061cc4d1879074ffb05a336a22cb37f8eb9b *turtl-{{site.releases.desktop}}-windows64.msi
      77 | $ cat desktop.sha256sums | grep windows64
      78 | fb57740d84e6838d83035949477c061cc4d1879074ffb05a336a22cb37f8eb9b *turtl-{{site.releases.desktop}}-windows64.msi
      79 | If those two hashes (fb57740...) match, your Turtl build has not been tampered with. 80 | If the two hashes above do not match, then do NOT install Turtl! 81 |
    8. 82 |
    83 |
    84 |
    85 | 86 | -------------------------------------------------------------------------------- /faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation 3 | title: "Frequently Asked Questions" 4 | permalink: "faq/" 5 | --- 6 | 7 | Frequently Asked Questions 8 | ========================== 9 | {% include toc.md %} 10 | 11 | ### When is Turtl for iOS coming? 12 | 13 | A lot of the background work for Turtl iOS is now complete, and it's a matter 14 | of putting in the time. Hopefully by 2019, Turtl will have an iOS app. 15 | [Track the feature here](https://github.com/turtl/tracker/issues/24)! 16 | 17 | ### How do I get the server running? 18 | 19 | People have issues with the Turtl server a lot. It's best to hop onto the 20 | [discussion board](https://groups.google.com/forum/#!forum/turtl) and ask there. 21 | Lots of people have asked for and received help on running the server on the 22 | discussion board. 23 | 24 | Keep in mind by running your own server you're going off the supported path. 25 | The maintainers of the project can't go out of their way to help you if 26 | you run into problems. The bulk of the work for the project happens on the 27 | clients (desktop, mobile) and helping with platform-specific server issues is 28 | not a priority. 29 | 30 | ### How do I delete my account? 31 | 32 | Use our handy [account delete](/users/delete/) page. 33 | 34 | ### Why isn't Turtl a native app? 35 | 36 | This is a small project, maintained by people with limited time, built mainly 37 | for the common good. 38 | 39 | Try building and maintaining three different apps on five different platforms 40 | with extremely limited spare time. You'll quickly realize how difficult it is 41 | (even just one codebase is hard enough). In the age we're in, it's completely 42 | ridiculous that mobile platforms have made it nearly impossible to reuse code 43 | across platforms. 44 | 45 | Think of Turtl's choice to use HTML5/webviews instead of going native on all 46 | platforms as a conscientious objection to the idea of forcing platforms down 47 | builder's throats. Really, we just don't have time to deal with all different 48 | codebases. 49 | 50 | ### Why does the note editor use Markdown? 51 | 52 | Turtl makes use of Markdown because it's a fairly easy format to learn and to 53 | write. Another big reason is that most rich text editors that are able to run 54 | inside of Turtl are horrible, buggy messes that would make the editing process 55 | a lot more painful. 56 | 57 | We're always looking for ways to improve the app, though. If you run across an 58 | HTML5 text editor that is clean, simple, mobile-friendly, and doesn't make you 59 | want to blow your brains out whenever you use it then [let us know](/contact). 60 | 61 | ### Can I contribute? 62 | 63 | Of course. For contributions of any significance (changing more than a few 64 | lines), we ask that you sign our Contributor License Agreement. Once that's 65 | done, you're free to contribute as much or as little as your heart desires. 66 | 67 | [Check out our contributing page for more details](/contributing). 68 | 69 | -------------------------------------------------------------------------------- /features.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Turtl features" 4 | permalink: "features/" 5 | --- 6 | 7 |
    8 |
    9 |
    10 |
    11 |
    12 |

    13 | Organize and share using Turtl's Spaces. 14 |

    15 |
    16 |
    17 |
    18 |

    19 | Choose who you share with and what they can do. 20 |

    21 |
    22 |
    23 |
    24 |

    25 | Search your notes with text queries or using tags. 26 |

    27 |
    28 | 29 |
    30 |
    31 |

    32 | Write notes easily using Markdown format 33 |

    34 |
    35 |
    36 |
    37 |

    38 | Turtl supports TeX math rendering. 39 |

    40 |
    41 |
    42 |
    43 |

    44 | Keep your profile backed up with import/export. 45 |

    46 |
    47 |
    48 |
    49 |
    50 | 51 |
    52 |
    53 |

    Full feature list

    54 |
      55 |
    • Different note types: text, bookmark, password, image, and file/document
    • 56 |
    • Client-side cryptography to keep all of your data safe
    • 57 |
    • Securely share with anyone without compromising the security of your data
    • 58 |
    • Sharing allows different permissions ranging from read-only to full ownership of shared content
    • 59 |
    • Find your notes easily. Turtl supports full-text search, filtering by tag (or lack of tag), and sort by create/edit date
    • 60 |
    • Attach photos, files, and documents to your notes. Files are stored securely just like the rest of your data.
    • 61 |
    • Browser extension makes bookmarking easy on desktop
    • 62 |
    • Share to Turtl on Android for easy bookmarking and file uploads
    • 63 |
    • Write notes in Markdown, an easy and natural way to format text
    • 64 |
    • TeX math expressions in notes for math people (surround them by $$ to use)
    • 65 |
    • Multiple translations (German, Spanish, French, and more)
    • 66 |
    • RTL text support for our Farsi/Hebrew/etc-speaking friends
    • 67 |
    • Export/import your entire profile for backup purposes or to move between servers
    • 68 |
    • Semi-offline mode (you only need to be connected to log in)
    • 69 |
    • A number of keyboard shortcuts for navigation the app without mouse (type ? in-app to see shortcuts)
    • 70 |
    • An open-source server allows you to host your own Turtl data
    • 71 |
    72 |
    73 |
    74 | 75 | -------------------------------------------------------------------------------- /images/favicon.128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/favicon.128.png -------------------------------------------------------------------------------- /images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/favicon.png -------------------------------------------------------------------------------- /images/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/font/fontello.woff2 -------------------------------------------------------------------------------- /images/home/devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/devices.png -------------------------------------------------------------------------------- /images/home/notebook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/notebook.jpg -------------------------------------------------------------------------------- /images/home/photo/collaborate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/photo/collaborate.jpg -------------------------------------------------------------------------------- /images/home/photo/organized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/photo/organized.jpg -------------------------------------------------------------------------------- /images/home/photo/security.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/photo/security.jpg -------------------------------------------------------------------------------- /images/home/screen/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/screen/export.png -------------------------------------------------------------------------------- /images/home/screen/invite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/screen/invite.png -------------------------------------------------------------------------------- /images/home/screen/markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/screen/markdown.png -------------------------------------------------------------------------------- /images/home/screen/math.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/screen/math.png -------------------------------------------------------------------------------- /images/home/screen/notes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/screen/notes.jpg -------------------------------------------------------------------------------- /images/home/screen/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/screen/search.jpg -------------------------------------------------------------------------------- /images/home/screen/spaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/home/screen/spaces.png -------------------------------------------------------------------------------- /images/logo-beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/logo-beta.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 39 | -------------------------------------------------------------------------------- /images/posts/turtl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turtl/www/4e0a9eb4dbc581cf8154c36a3393c6a803291b58/images/posts/turtl.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | body_class: "home" 4 | show_splash: true 5 | title: 'Turtl: The secure, collaborative notebook' 6 | --- 7 | 8 | 11 | 12 |
    13 |
     
    14 |
    15 |
    16 |
    17 |

    18 | The secure, collaborative notebook 19 |

    20 | 21 | Download Turtl 22 | » 23 | 24 |
    25 |
    26 | 27 |
    28 | 34 |
    35 |
    36 |
    37 | 38 |
    39 | 40 |
    41 |
    42 |

    Organize your life

    43 |

    44 | Whether it's bookmarks or passwords, files or shopping 45 | lists...Turtl organizes it all and makes it easy to find later. 46 | Sync across your devices. Leave nothing behind. 47 |

    48 |
    49 |
    50 |
    51 | 52 |
    53 | 54 |
    55 |
    56 |

    Protect your data

    57 |

    58 | Turtl uses high-end cryptography to protect your data. 59 | Whether you're worried about information leaks, competitive 60 | advantage, or blanket government surveillance, Turtl works 61 | hard to make sure only you, and those you choose, can see 62 | your data. 63 |

    64 |

    65 | Read more about Turtl's security » 66 |

    67 |
    68 |
    69 |
    70 | 71 |
    72 | 73 |
    74 |
    75 |

    Collaborate and share

    76 |

    77 | Just because Turtl is secure and private doesn't mean you 78 | can't share with your teammates or family. Choose who you 79 | want to have access to your data without compromising your 80 | security. 81 |

    82 |
    83 |
    84 |
    85 | 86 |
    87 |
    88 |
    89 |
    90 |
    91 |

    92 | Organize and share using Turtl's Spaces. 93 |

    94 |
    95 | 103 |
    104 |
    105 |

    106 | Search your notes with text queries or using tags. 107 |

    108 |
    109 | 110 |
    111 |
    112 |

    113 | Write notes easily using Markdown format 114 |

    115 |
    116 | 130 |
    131 | 134 |
    135 |
    136 | 137 |
    138 |
    139 |
    140 |
    141 |

    142 | We love privacy, but don't take our word for it. 143 | Turtl is open source 144 | and you can read the code yourself! 145 |
    146 |
    147 |

    148 | Want more control of your data? 149 | Install your own Turtl server at home or at work. 150 |
    151 |
    152 |

    153 | Turtl is a community project, and we love when people like you help out! 154 |
    155 |
    156 |
    157 |
    158 | 159 |
    160 |
    161 |
    162 |

    163 |

    164 | Don't want to pay for our premium service?
    165 | Consider making a donation to support the project! 166 |

    167 |
    168 | Make a donation » 169 |
    170 |
    171 |
    172 | 173 | -------------------------------------------------------------------------------- /js/controller-sig.js: -------------------------------------------------------------------------------- 1 | var CLASigController = Composer.Controller.extend({ 2 | elements: { 3 | 'form': 'el_form', 4 | 'ul.error': 'el_errlist', 5 | 'input[name=type]': 'inp_type', 6 | 'input[name=entity]': 'inp_entity', 7 | 'input[name=fullname]': 'inp_fullname', 8 | 'input[name=email]': 'inp_email', 9 | 'input[name=address1]': 'inp_address1', 10 | 'input[name=address2]': 'inp_address2', 11 | 'input[name=city]': 'inp_city', 12 | 'input[name=state]': 'inp_state', 13 | 'input[name=zip]': 'inp_zip', 14 | 'select[name=country]': 'inp_country', 15 | //'input[name=phone]': 'inp_phone', 16 | 'input[name=github]': 'inp_github', 17 | 'input[name=website]': 'inp_website', 18 | 'input[name=sign]': 'inp_sign', 19 | 'input[type=submit]': 'btn_submit' 20 | }, 21 | 22 | events: { 23 | 'submit form': 'submit' 24 | }, 25 | 26 | redirect: window.location.protocol+'//'+window.location.hostname+'/contributing/sign-thanks', 27 | redirect_err: window.location.protocol+'//'+window.location.hostname+'/contributing/sign-error', 28 | 29 | init: function() 30 | { 31 | this.render(); 32 | }, 33 | 34 | render: function() 35 | { 36 | // CSS should hide this field, but just in case... 37 | this.inp_website.set('placeholder', 'LEAVE BLANK. Used for catching spam'); 38 | new Element('input') 39 | .set('type', 'hidden') 40 | .set('name', 'redirect') 41 | .set('value', this.redirect) 42 | .inject(this.el_form); 43 | new Element('input') 44 | .set('type', 'hidden') 45 | .set('name', 'redirect-err') 46 | .set('value', this.redirect_err) 47 | .inject(this.el_form); 48 | }, 49 | 50 | submit: function(e) 51 | { 52 | if(e) e.stop(); 53 | var inputs = Object.keys(this).filter(function(k) { return k.match(/^inp_/); }); 54 | var data = {}; 55 | 56 | // if the website field is filled in, make this a fake post 57 | var fake_post = false; 58 | 59 | inputs.forEach(function(key) { 60 | var el = this[key]; 61 | var val = (el && el.get('value')) || null; 62 | data[key.replace(/^inp_/, '')] = val; 63 | }.bind(this)); 64 | 65 | var req = [ 66 | {field: 'fullname', name: 'full name'}, 67 | {field: 'email', name: 'email address'}, 68 | {field: 'address1', name: 'address'}, 69 | {field: 'city', name: 'city'}, 70 | {field: 'country', name: 'country'}, 71 | //{field: 'phone', name: 'phone number'}, 72 | {field: 'github', name: 'github username'}, 73 | ]; 74 | if(this.inp_type.get('value') == 'ecla') 75 | { 76 | req.unshift({field: 'entity', name: 'company/organization/entity name'}); 77 | } 78 | 79 | this.el.getElements('.error-field').forEach(function(el) { el.removeClass('error-field'); }); 80 | this.el_errlist.addClass('hide'); 81 | this.el_errlist.set('html', ''); 82 | 83 | var errors = []; 84 | req.forEach(function(row) { 85 | var key = row.field; 86 | var name = row.name; 87 | if(data[key] && data[key].trim()) return; 88 | errors.push([this['inp_'+key], 'Please give your '+name]); 89 | }.bind(this)); 90 | 91 | if(data.sign !== 'I AGREE') errors.push([this.inp_sign, 'Please type "I AGREE" in the signature box']); 92 | if(data.website) fake_post = true; 93 | 94 | if(errors.length) 95 | { 96 | this.el_errlist.removeClass('hide'); 97 | errors.forEach(function(err) { 98 | var inp = err[0]; 99 | var msg = err[1]; 100 | inp.addClass('error-field'); 101 | new Element('li').set('html', msg).inject(this.el_errlist); 102 | }.bind(this)); 103 | var first = this.el.getElement('input.error-field, select.error-field'); 104 | if(first) first.focus(); 105 | return; 106 | } 107 | 108 | if(fake_post) 109 | { 110 | window.location = this.redirect; 111 | return; 112 | } 113 | 114 | this.el_form.submit(); 115 | } 116 | }); 117 | 118 | -------------------------------------------------------------------------------- /js/delete-account.js: -------------------------------------------------------------------------------- 1 | const DeleteAccountController = Composer.Controller.extend({ 2 | elements: { 3 | '.top-msg': '$msg', 4 | 'form': '$form', 5 | 'input[name=email]': '$inp_email', 6 | }, 7 | 8 | events: { 9 | 'submit form': 'submit', 10 | }, 11 | 12 | model: null, 13 | init: function() { 14 | this.model = new DeleteAccountModel(); 15 | }, 16 | 17 | submit: function(e) { 18 | if(e) e.stop(); 19 | const email = this.$inp_email.get('value'); 20 | this.model.set({email: email}); 21 | return this.model.start() 22 | .bind(this) 23 | .then(function() { 24 | this.success('We have sent you an email with your deletion link.'); 25 | }) 26 | .catch(function(err) { 27 | this.error(err); 28 | }); 29 | }, 30 | 31 | error: function(msg) { 32 | this.$msg.set('html', '
    '+msg+'
    '); 33 | }, 34 | 35 | success: function(msg) { 36 | this.$msg.set('html', '
    '+msg+'
    '); 37 | }, 38 | }); 39 | 40 | const DeleteAccountModel = Composer.Model.extend({ 41 | get_url: function() { 42 | const api_url = window.location.toString().match(/turtl\.loc/) ? 43 | 'http://api.turtl.loc:8181' : 44 | 'https://apiv3.turtlapp.com'; 45 | return api_url+'/users/delete/'+encodeURIComponent(this.get('email')); 46 | }, 47 | 48 | start: function() { 49 | var req = { 50 | method: 'post', 51 | url: this.get_url(), 52 | }; 53 | return Sexhr(req) 54 | .bind(this) 55 | .then(function() { 56 | return true; 57 | }) 58 | .catch(this.err.bind(this)); 59 | }, 60 | 61 | check_email: function(email) { 62 | var req = { 63 | url: this.get_url()+'/email/'+encodeURIComponent(email), 64 | }; 65 | return Sexhr(req) 66 | .bind(this) 67 | .spread(function(res, _xhr) { 68 | var emailobj = JSON.parse(res); 69 | return emailobj.username; 70 | }) 71 | .catch(this.err.bind(this)); 72 | }, 73 | 74 | update: function() { 75 | var req = { 76 | url: this.get_url(), 77 | method: 'PUT', 78 | headers: {'Content-Type': 'application/json'}, 79 | data: JSON.stringify(this.toJSON()), 80 | }; 81 | return Sexhr(req) 82 | .bind(this) 83 | .spread(function(payment, _xhr) { 84 | this.reset(JSON.parse(payment)); 85 | return this; 86 | }) 87 | .catch(this.err.bind(this)); 88 | }, 89 | 90 | cancel: function() { 91 | var req = { 92 | url: this.get_url(), 93 | method: 'DELETE', 94 | }; 95 | var modal = UIkit.modal($('loading-modal')); 96 | modal.show(); 97 | return Sexhr(req) 98 | .catch(this.err.bind(this)) 99 | .finally(function() { 100 | modal.hide(); 101 | }); 102 | }, 103 | 104 | err: function(errobj) { 105 | console.log(errobj); 106 | throw JSON.parse(errobj.msg).error.message; 107 | }, 108 | }); 109 | 110 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | var app = { 2 | init: function() { 3 | app.init_signature(); 4 | }, 5 | 6 | init_signature: function() { 7 | document.getElements('.sign-cla').forEach(function(el) { 8 | new CLASigController({el: el}); 9 | }); 10 | } 11 | }; 12 | 13 | window.addEvent('domready', function() { 14 | app.init(); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /js/manage.js: -------------------------------------------------------------------------------- 1 | const ManageController = Composer.Controller.extend({ 2 | elements: { 3 | '.top-msg': '$msg', 4 | '.form-container': '$form_container', 5 | 'input[name=billing-email]': '$email_billing', 6 | 'input[name=account-email]': '$email_account', 7 | '.linked-icon': '$icon', 8 | }, 9 | 10 | events: { 11 | 'submit form': 'submit', 12 | 'click a[href=#cancel]': 'cancel_subscription', 13 | 'input input[name=account-email]': 'check_email', 14 | }, 15 | 16 | model: null, 17 | init: function() { 18 | var id = Composer.Router.prototype.get_param(window.location.search, 'id'); 19 | var token = Composer.Router.prototype.get_param(window.location.search, 'token'); 20 | if(!id || !token) { 21 | this.error('Your payment information could not be found. Please verify you have the correct URL.'); 22 | this.hide(true); 23 | return; 24 | } 25 | this.model = new ManageModel({id: id, manage_token: token}); 26 | this.model.grab() 27 | .bind(this) 28 | .then(this.render.bind(this)) 29 | .catch(function(err) { 30 | this.error(err); 31 | this.hide(true); 32 | }); 33 | }, 34 | 35 | render: function() { 36 | var account_email = this.model.get('account_email'); 37 | this.$email_billing.set('value', this.model.get('email')); 38 | this.$email_account.set('value', account_email); 39 | if(account_email) { 40 | this.$icon.setStyles({display: ''}); 41 | } else { 42 | this.$icon.setStyles({display: 'none'}); 43 | } 44 | }, 45 | 46 | submit: function(e) { 47 | if(e) e.stop(); 48 | var billing_email = this.$email_billing.get('value'); 49 | var account_email = this.$email_account.get('value'); 50 | this.model.set({email: billing_email, account_email: account_email}); 51 | return this.model.update() 52 | .bind(this) 53 | .then(function(saved) { 54 | var account_email = this.model.get('account_email'); 55 | if(account_email) { 56 | var extra = ' and has been linked to the Turtl account '+account_email+'.'; 57 | } else { 58 | var extra = ', however there is not a Turtl account linked to your billing information.'; 59 | } 60 | this.success('Your billing information has been saved'+extra); 61 | }) 62 | .catch(function(err) { 63 | this.error(err); 64 | }); 65 | }, 66 | 67 | error: function(msg) { 68 | this.$msg.set('html', '
    '+msg+'
    '); 69 | }, 70 | 71 | success: function(msg) { 72 | this.$msg.set('html', '
    '+msg+'
    '); 73 | }, 74 | 75 | hide: function(truefalse) { 76 | if(truefalse) { 77 | this.$form_container.addClass('uk-hidden'); 78 | } else { 79 | this.$form_container.removeClass('uk-hidden'); 80 | } 81 | }, 82 | 83 | check_email: function(e) { 84 | if(this._timeout) { 85 | clearTimeout(this._timeout); 86 | this._timeout = null; 87 | } 88 | this._timeout = setTimeout(function() { 89 | this.model.check_email(this.$email_account.get('value')) 90 | .bind(this) 91 | .catch(function(e) { 92 | console.error('err: ', e); 93 | return false; 94 | }) 95 | .then(function(email) { 96 | if(!email) { 97 | this.$icon.setStyles({display: 'none'}); 98 | return false; 99 | } 100 | this.$icon.setStyles({display: ''}); 101 | this.$email_account.set('value', email); 102 | }); 103 | console.log('check email'); 104 | }.bind(this), 500); 105 | }, 106 | 107 | cancel_subscription: function(e) { 108 | if(e) e.stop(); 109 | if(!confirm('Are you sure you want to cancel your Turtl subscription?')) { 110 | return false; 111 | } 112 | this.model.cancel() 113 | .bind(this) 114 | .then(function() { 115 | this.el.set('html', '

    Your subscription has been cancelled

    Thanks for giving Turtl a try!

    Return to the homepage »'); 116 | }) 117 | .catch(function(err) { 118 | this.error('There was a problem cancelling: '+err.message); 119 | }); 120 | }, 121 | }); 122 | 123 | const ManageModel = Composer.Model.extend({ 124 | get_url: function() { 125 | const api_url = window.location.toString().match(/turtl\.loc/) ? 126 | 'http://api.turtl.loc:8181' : 127 | 'https://apiv3.turtlapp.com'; 128 | return api_url+'/payment/'+encodeURIComponent(this.id())+'/'+encodeURIComponent(this.get('manage_token')); 129 | }, 130 | 131 | grab: function() { 132 | var req = { 133 | url: this.get_url(), 134 | }; 135 | return Sexhr(req) 136 | .bind(this) 137 | .spread(function(payment, _xhr) { 138 | this.set(JSON.parse(payment)); 139 | return this; 140 | }) 141 | .catch(this.err.bind(this)); 142 | }, 143 | 144 | check_email: function(email) { 145 | var req = { 146 | url: this.get_url()+'/email/'+encodeURIComponent(email), 147 | }; 148 | return Sexhr(req) 149 | .bind(this) 150 | .spread(function(res, _xhr) { 151 | var emailobj = JSON.parse(res); 152 | return emailobj.username; 153 | }) 154 | .catch(this.err.bind(this)); 155 | }, 156 | 157 | update: function() { 158 | var req = { 159 | url: this.get_url(), 160 | method: 'PUT', 161 | headers: {'Content-Type': 'application/json'}, 162 | data: JSON.stringify(this.toJSON()), 163 | }; 164 | return Sexhr(req) 165 | .bind(this) 166 | .spread(function(payment, _xhr) { 167 | this.reset(JSON.parse(payment)); 168 | return this; 169 | }) 170 | .catch(this.err.bind(this)); 171 | }, 172 | 173 | cancel: function() { 174 | var req = { 175 | url: this.get_url(), 176 | method: 'DELETE', 177 | }; 178 | var modal = UIkit.modal($('loading-modal')); 179 | modal.show(); 180 | return Sexhr(req) 181 | .catch(this.err.bind(this)) 182 | .finally(function() { 183 | modal.hide(); 184 | }); 185 | }, 186 | 187 | err: function(errobj) { 188 | console.log(errobj); 189 | throw JSON.parse(errobj.msg).error.message; 190 | }, 191 | }); 192 | 193 | -------------------------------------------------------------------------------- /js/mootools-more-1.6.0.js: -------------------------------------------------------------------------------- 1 | /* MooTools: the javascript framework. license: MIT-style license. copyright: Copyright (c) 2006-2018 [Valerio Proietti](https://mootools.net/).*/ 2 | /*! 3 | Web Build: https://mootools.net/more/builder/51368179dd40043585fe86ca0c128850 4 | */ 5 | /* 6 | --- 7 | 8 | script: More.js 9 | 10 | name: More 11 | 12 | description: MooTools More 13 | 14 | license: MIT-style license 15 | 16 | authors: 17 | - Guillermo Rauch 18 | - Thomas Aylott 19 | - Scott Kyle 20 | - Arian Stolwijk 21 | - Tim Wienk 22 | - Christoph Pojer 23 | - Aaron Newton 24 | - Jacob Thornton 25 | 26 | requires: 27 | - Core/MooTools 28 | 29 | provides: [MooTools.More] 30 | 31 | ... 32 | */ 33 | 34 | MooTools.More = { 35 | version: '1.6.0', 36 | build: '45b71db70f879781a7e0b0d3fb3bb1307c2521eb' 37 | }; 38 | 39 | /* 40 | --- 41 | 42 | name: Events.Pseudos 43 | 44 | description: Adds the functionality to add pseudo events 45 | 46 | license: MIT-style license 47 | 48 | authors: 49 | - Arian Stolwijk 50 | 51 | requires: [Core/Class.Extras, Core/Slick.Parser, MooTools.More] 52 | 53 | provides: [Events.Pseudos] 54 | 55 | ... 56 | */ 57 | 58 | (function(){ 59 | 60 | Events.Pseudos = function(pseudos, addEvent, removeEvent){ 61 | 62 | var storeKey = '_monitorEvents:'; 63 | 64 | var storageOf = function(object){ 65 | return { 66 | store: object.store ? function(key, value){ 67 | object.store(storeKey + key, value); 68 | } : function(key, value){ 69 | (object._monitorEvents || (object._monitorEvents = {}))[key] = value; 70 | }, 71 | retrieve: object.retrieve ? function(key, dflt){ 72 | return object.retrieve(storeKey + key, dflt); 73 | } : function(key, dflt){ 74 | if (!object._monitorEvents) return dflt; 75 | return object._monitorEvents[key] || dflt; 76 | } 77 | }; 78 | }; 79 | 80 | var splitType = function(type){ 81 | if (type.indexOf(':') == -1 || !pseudos) return null; 82 | 83 | var parsed = Slick.parse(type).expressions[0][0], 84 | parsedPseudos = parsed.pseudos, 85 | l = parsedPseudos.length, 86 | splits = []; 87 | 88 | while (l--){ 89 | var pseudo = parsedPseudos[l].key, 90 | listener = pseudos[pseudo]; 91 | if (listener != null) splits.push({ 92 | event: parsed.tag, 93 | value: parsedPseudos[l].value, 94 | pseudo: pseudo, 95 | original: type, 96 | listener: listener 97 | }); 98 | } 99 | return splits.length ? splits : null; 100 | }; 101 | 102 | return { 103 | 104 | addEvent: function(type, fn, internal){ 105 | var split = splitType(type); 106 | if (!split) return addEvent.call(this, type, fn, internal); 107 | 108 | var storage = storageOf(this), 109 | events = storage.retrieve(type, []), 110 | eventType = split[0].event, 111 | args = Array.slice(arguments, 2), 112 | stack = fn, 113 | self = this; 114 | 115 | split.each(function(item){ 116 | var listener = item.listener, 117 | stackFn = stack; 118 | if (listener == false) eventType += ':' + item.pseudo + '(' + item.value + ')'; 119 | else stack = function(){ 120 | listener.call(self, item, stackFn, arguments, stack); 121 | }; 122 | }); 123 | 124 | events.include({type: eventType, event: fn, monitor: stack}); 125 | storage.store(type, events); 126 | 127 | if (type != eventType) addEvent.apply(this, [type, fn].concat(args)); 128 | return addEvent.apply(this, [eventType, stack].concat(args)); 129 | }, 130 | 131 | removeEvent: function(type, fn){ 132 | var split = splitType(type); 133 | if (!split) return removeEvent.call(this, type, fn); 134 | 135 | var storage = storageOf(this), 136 | events = storage.retrieve(type); 137 | if (!events) return this; 138 | 139 | var args = Array.slice(arguments, 2); 140 | 141 | removeEvent.apply(this, [type, fn].concat(args)); 142 | events.each(function(monitor, i){ 143 | if (!fn || monitor.event == fn) removeEvent.apply(this, [monitor.type, monitor.monitor].concat(args)); 144 | delete events[i]; 145 | }, this); 146 | 147 | storage.store(type, events); 148 | return this; 149 | } 150 | 151 | }; 152 | 153 | }; 154 | 155 | var pseudos = { 156 | 157 | once: function(split, fn, args, monitor){ 158 | fn.apply(this, args); 159 | this.removeEvent(split.event, monitor) 160 | .removeEvent(split.original, fn); 161 | }, 162 | 163 | throttle: function(split, fn, args){ 164 | if (!fn._throttled){ 165 | fn.apply(this, args); 166 | fn._throttled = setTimeout(function(){ 167 | fn._throttled = false; 168 | }, split.value || 250); 169 | } 170 | }, 171 | 172 | pause: function(split, fn, args){ 173 | clearTimeout(fn._pause); 174 | fn._pause = fn.delay(split.value || 250, this, args); 175 | } 176 | 177 | }; 178 | 179 | Events.definePseudo = function(key, listener){ 180 | pseudos[key] = listener; 181 | return this; 182 | }; 183 | 184 | Events.lookupPseudo = function(key){ 185 | return pseudos[key]; 186 | }; 187 | 188 | var proto = Events.prototype; 189 | Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent)); 190 | 191 | ['Request', 'Fx'].each(function(klass){ 192 | if (this[klass]) this[klass].implement(Events.prototype); 193 | }); 194 | 195 | })(); 196 | 197 | /* 198 | --- 199 | 200 | name: Element.Event.Pseudos 201 | 202 | description: Adds the functionality to add pseudo events for Elements 203 | 204 | license: MIT-style license 205 | 206 | authors: 207 | - Arian Stolwijk 208 | 209 | requires: [Core/Element.Event, Core/Element.Delegation, Events.Pseudos] 210 | 211 | provides: [Element.Event.Pseudos, Element.Delegation.Pseudo] 212 | 213 | ... 214 | */ 215 | 216 | (function(){ 217 | 218 | var pseudos = {relay: false}, 219 | copyFromEvents = ['once', 'throttle', 'pause'], 220 | count = copyFromEvents.length; 221 | 222 | while (count--) pseudos[copyFromEvents[count]] = Events.lookupPseudo(copyFromEvents[count]); 223 | 224 | DOMEvent.definePseudo = function(key, listener){ 225 | pseudos[key] = listener; 226 | return this; 227 | }; 228 | 229 | var proto = Element.prototype; 230 | [Element, Window, Document].invoke('implement', Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent)); 231 | 232 | })(); 233 | 234 | /* 235 | --- 236 | 237 | name: Element.Event.Pseudos.Keys 238 | 239 | description: Adds functionality fire events if certain keycombinations are pressed 240 | 241 | license: MIT-style license 242 | 243 | authors: 244 | - Arian Stolwijk 245 | 246 | requires: [Element.Event.Pseudos] 247 | 248 | provides: [Element.Event.Pseudos.Keys] 249 | 250 | ... 251 | */ 252 | 253 | (function(){ 254 | 255 | var keysStoreKey = '$moo:keys-pressed', 256 | keysKeyupStoreKey = '$moo:keys-keyup'; 257 | 258 | 259 | DOMEvent.definePseudo('keys', function(split, fn, args){ 260 | 261 | var event = args[0], 262 | keys = [], 263 | pressed = this.retrieve(keysStoreKey, []), 264 | value = split.value; 265 | 266 | if (value != '+') keys.append(value.replace('++', function(){ 267 | keys.push('+'); // shift++ and shift+++a 268 | return ''; 269 | }).split('+')); 270 | else keys = ['+']; 271 | 272 | pressed.include(event.key); 273 | 274 | if (keys.every(function(key){ 275 | return pressed.contains(key); 276 | })) fn.apply(this, args); 277 | 278 | this.store(keysStoreKey, pressed); 279 | 280 | if (!this.retrieve(keysKeyupStoreKey)){ 281 | var keyup = function(event){ 282 | (function(){ 283 | pressed = this.retrieve(keysStoreKey, []).erase(event.key); 284 | this.store(keysStoreKey, pressed); 285 | }).delay(0, this); // Fix for IE 286 | }; 287 | this.store(keysKeyupStoreKey, keyup).addEvent('keyup', keyup); 288 | } 289 | 290 | }); 291 | 292 | DOMEvent.defineKeys({ 293 | '16': 'shift', 294 | '17': 'control', 295 | '18': 'alt', 296 | '20': 'capslock', 297 | '33': 'pageup', 298 | '34': 'pagedown', 299 | '35': 'end', 300 | '36': 'home', 301 | '144': 'numlock', 302 | '145': 'scrolllock', 303 | '186': ';', 304 | '187': '=', 305 | '188': ',', 306 | '190': '.', 307 | '191': '/', 308 | '192': '`', 309 | '219': '[', 310 | '220': '\\', 311 | '221': ']', 312 | '222': "'", 313 | '107': '+', 314 | '109': '-', // subtract 315 | '189': '-' // dash 316 | }); 317 | 318 | })(); 319 | 320 | /* 321 | --- 322 | 323 | script: String.Extras.js 324 | 325 | name: String.Extras 326 | 327 | description: Extends the String native object to include methods useful in managing various kinds of strings (query strings, urls, html, etc). 328 | 329 | license: MIT-style license 330 | 331 | authors: 332 | - Aaron Newton 333 | - Guillermo Rauch 334 | - Christopher Pitt 335 | 336 | requires: 337 | - Core/String 338 | - Core/Array 339 | - MooTools.More 340 | 341 | provides: [String.Extras] 342 | 343 | ... 344 | */ 345 | 346 | (function(){ 347 | 348 | var special = { 349 | 'a': /[àáâãäåăą]/g, 350 | 'A': /[ÀÁÂÃÄÅĂĄ]/g, 351 | 'c': /[ćčç]/g, 352 | 'C': /[ĆČÇ]/g, 353 | 'd': /[ďđ]/g, 354 | 'D': /[ĎÐ]/g, 355 | 'e': /[èéêëěę]/g, 356 | 'E': /[ÈÉÊËĚĘ]/g, 357 | 'g': /[ğ]/g, 358 | 'G': /[Ğ]/g, 359 | 'i': /[ìíîï]/g, 360 | 'I': /[ÌÍÎÏ]/g, 361 | 'l': /[ĺľł]/g, 362 | 'L': /[ĹĽŁ]/g, 363 | 'n': /[ñňń]/g, 364 | 'N': /[ÑŇŃ]/g, 365 | 'o': /[òóôõöøő]/g, 366 | 'O': /[ÒÓÔÕÖØ]/g, 367 | 'r': /[řŕ]/g, 368 | 'R': /[ŘŔ]/g, 369 | 's': /[ššş]/g, 370 | 'S': /[ŠŞŚ]/g, 371 | 't': /[ťţ]/g, 372 | 'T': /[ŤŢ]/g, 373 | 'u': /[ùúûůüµ]/g, 374 | 'U': /[ÙÚÛŮÜ]/g, 375 | 'y': /[ÿý]/g, 376 | 'Y': /[ŸÝ]/g, 377 | 'z': /[žźż]/g, 378 | 'Z': /[ŽŹŻ]/g, 379 | 'th': /[þ]/g, 380 | 'TH': /[Þ]/g, 381 | 'dh': /[ð]/g, 382 | 'DH': /[Ð]/g, 383 | 'ss': /[ß]/g, 384 | 'oe': /[œ]/g, 385 | 'OE': /[Œ]/g, 386 | 'ae': /[æ]/g, 387 | 'AE': /[Æ]/g 388 | }, 389 | 390 | tidy = { 391 | ' ': /[\xa0\u2002\u2003\u2009]/g, 392 | '*': /[\xb7]/g, 393 | '\'': /[\u2018\u2019]/g, 394 | '"': /[\u201c\u201d]/g, 395 | '...': /[\u2026]/g, 396 | '-': /[\u2013]/g, 397 | // '--': /[\u2014]/g, 398 | '»': /[\uFFFD]/g 399 | }, 400 | 401 | conversions = { 402 | ms: 1, 403 | s: 1000, 404 | m: 6e4, 405 | h: 36e5 406 | }, 407 | 408 | findUnits = /(\d*.?\d+)([msh]+)/; 409 | 410 | var walk = function(string, replacements){ 411 | var result = string, key; 412 | for (key in replacements) result = result.replace(replacements[key], key); 413 | return result; 414 | }; 415 | 416 | var getRegexForTag = function(tag, contents){ 417 | tag = tag || (contents ? '' : '\\w+'); 418 | var regstr = contents ? '<' + tag + '(?!\\w)[^>]*>([\\s\\S]*?)<\/' + tag + '(?!\\w)>' : '<\/?' + tag + '\/?>|<' + tag + '[\\s|\/][^>]*>'; 419 | return new RegExp(regstr, 'gi'); 420 | }; 421 | 422 | String.implement({ 423 | 424 | standardize: function(){ 425 | return walk(this, special); 426 | }, 427 | 428 | repeat: function(times){ 429 | return new Array(times + 1).join(this); 430 | }, 431 | 432 | pad: function(length, str, direction){ 433 | if (this.length >= length) return this; 434 | 435 | var pad = (str == null ? ' ' : '' + str) 436 | .repeat(length - this.length) 437 | .substr(0, length - this.length); 438 | 439 | if (!direction || direction == 'right') return this + pad; 440 | if (direction == 'left') return pad + this; 441 | 442 | return pad.substr(0, (pad.length / 2).floor()) + this + pad.substr(0, (pad.length / 2).ceil()); 443 | }, 444 | 445 | getTags: function(tag, contents){ 446 | return this.match(getRegexForTag(tag, contents)) || []; 447 | }, 448 | 449 | stripTags: function(tag, contents){ 450 | return this.replace(getRegexForTag(tag, contents), ''); 451 | }, 452 | 453 | tidy: function(){ 454 | return walk(this, tidy); 455 | }, 456 | 457 | truncate: function(max, trail, atChar){ 458 | var string = this; 459 | if (trail == null && arguments.length == 1) trail = '…'; 460 | if (string.length > max){ 461 | string = string.substring(0, max); 462 | if (atChar){ 463 | var index = string.lastIndexOf(atChar); 464 | if (index != -1) string = string.substr(0, index); 465 | } 466 | if (trail) string += trail; 467 | } 468 | return string; 469 | }, 470 | 471 | ms: function(){ 472 | // "Borrowed" from https://gist.github.com/1503944 473 | var units = findUnits.exec(this); 474 | if (units == null) return Number(this); 475 | return Number(units[1]) * conversions[units[2]]; 476 | } 477 | 478 | }); 479 | 480 | })(); 481 | 482 | /* 483 | --- 484 | 485 | script: Element.Forms.js 486 | 487 | name: Element.Forms 488 | 489 | description: Extends the Element native object to include methods useful in managing inputs. 490 | 491 | license: MIT-style license 492 | 493 | authors: 494 | - Aaron Newton 495 | 496 | requires: 497 | - Core/Element 498 | - String.Extras 499 | - MooTools.More 500 | 501 | provides: [Element.Forms] 502 | 503 | ... 504 | */ 505 | 506 | Element.implement({ 507 | 508 | tidy: function(){ 509 | this.set('value', this.get('value').tidy()); 510 | }, 511 | 512 | getTextInRange: function(start, end){ 513 | return this.get('value').substring(start, end); 514 | }, 515 | 516 | getSelectedText: function(){ 517 | if (this.setSelectionRange) return this.getTextInRange(this.getSelectionStart(), this.getSelectionEnd()); 518 | return document.selection.createRange().text; 519 | }, 520 | 521 | getSelectedRange: function(){ 522 | if (this.selectionStart != null){ 523 | return { 524 | start: this.selectionStart, 525 | end: this.selectionEnd 526 | }; 527 | } 528 | 529 | var pos = { 530 | start: 0, 531 | end: 0 532 | }; 533 | var range = this.getDocument().selection.createRange(); 534 | if (!range || range.parentElement() != this) return pos; 535 | var duplicate = range.duplicate(); 536 | 537 | if (this.type == 'text'){ 538 | pos.start = 0 - duplicate.moveStart('character', -100000); 539 | pos.end = pos.start + range.text.length; 540 | } else { 541 | var value = this.get('value'); 542 | var offset = value.length; 543 | duplicate.moveToElementText(this); 544 | duplicate.setEndPoint('StartToEnd', range); 545 | if (duplicate.text.length) offset -= value.match(/[\n\r]*$/)[0].length; 546 | pos.end = offset - duplicate.text.length; 547 | duplicate.setEndPoint('StartToStart', range); 548 | pos.start = offset - duplicate.text.length; 549 | } 550 | return pos; 551 | }, 552 | 553 | getSelectionStart: function(){ 554 | return this.getSelectedRange().start; 555 | }, 556 | 557 | getSelectionEnd: function(){ 558 | return this.getSelectedRange().end; 559 | }, 560 | 561 | setCaretPosition: function(pos){ 562 | if (pos == 'end') pos = this.get('value').length; 563 | this.selectRange(pos, pos); 564 | return this; 565 | }, 566 | 567 | getCaretPosition: function(){ 568 | return this.getSelectedRange().start; 569 | }, 570 | 571 | selectRange: function(start, end){ 572 | if (this.setSelectionRange){ 573 | this.focus(); 574 | this.setSelectionRange(start, end); 575 | } else { 576 | var value = this.get('value'); 577 | var diff = value.substr(start, end - start).replace(/\r/g, '').length; 578 | start = value.substr(0, start).replace(/\r/g, '').length; 579 | var range = this.createTextRange(); 580 | range.collapse(true); 581 | range.moveEnd('character', start + diff); 582 | range.moveStart('character', start); 583 | range.select(); 584 | } 585 | return this; 586 | }, 587 | 588 | insertAtCursor: function(value, select){ 589 | var pos = this.getSelectedRange(); 590 | var text = this.get('value'); 591 | this.set('value', text.substring(0, pos.start) + value + text.substring(pos.end, text.length)); 592 | if (select !== false) this.selectRange(pos.start, pos.start + value.length); 593 | else this.setCaretPosition(pos.start + value.length); 594 | return this; 595 | }, 596 | 597 | insertAroundCursor: function(options, select){ 598 | options = Object.append({ 599 | before: '', 600 | defaultMiddle: '', 601 | after: '' 602 | }, options); 603 | 604 | var value = this.getSelectedText() || options.defaultMiddle; 605 | var pos = this.getSelectedRange(); 606 | var text = this.get('value'); 607 | 608 | if (pos.start == pos.end){ 609 | this.set('value', text.substring(0, pos.start) + options.before + value + options.after + text.substring(pos.end, text.length)); 610 | this.selectRange(pos.start + options.before.length, pos.end + options.before.length + value.length); 611 | } else { 612 | var current = text.substring(pos.start, pos.end); 613 | this.set('value', text.substring(0, pos.start) + options.before + current + options.after + text.substring(pos.end, text.length)); 614 | var selStart = pos.start + options.before.length; 615 | if (select !== false) this.selectRange(selStart, selStart + current.length); 616 | else this.setCaretPosition(selStart + text.length); 617 | } 618 | return this; 619 | } 620 | 621 | }); 622 | 623 | /* 624 | --- 625 | 626 | script: Fx.Scroll.js 627 | 628 | name: Fx.Scroll 629 | 630 | description: Effect to smoothly scroll any element, including the window. 631 | 632 | license: MIT-style license 633 | 634 | authors: 635 | - Valerio Proietti 636 | 637 | requires: 638 | - Core/Fx 639 | - Core/Element.Event 640 | - Core/Element.Dimensions 641 | - MooTools.More 642 | 643 | provides: [Fx.Scroll] 644 | 645 | ... 646 | */ 647 | 648 | (function(){ 649 | 650 | Fx.Scroll = new Class({ 651 | 652 | Extends: Fx, 653 | 654 | options: { 655 | offset: {x: 0, y: 0}, 656 | wheelStops: true 657 | }, 658 | 659 | initialize: function(element, options){ 660 | this.element = this.subject = document.id(element); 661 | this.parent(options); 662 | 663 | if (typeOf(this.element) != 'element') this.element = document.id(this.element.getDocument().body); 664 | 665 | if (this.options.wheelStops){ 666 | var stopper = this.element, 667 | cancel = this.cancel.pass(false, this); 668 | this.addEvent('start', function(){ 669 | stopper.addEvent('mousewheel', cancel); 670 | }, true); 671 | this.addEvent('complete', function(){ 672 | stopper.removeEvent('mousewheel', cancel); 673 | }, true); 674 | } 675 | }, 676 | 677 | set: function(){ 678 | var now = Array.flatten(arguments); 679 | this.element.scrollTo(now[0], now[1]); 680 | return this; 681 | }, 682 | 683 | compute: function(from, to, delta){ 684 | return [0, 1].map(function(i){ 685 | return Fx.compute(from[i], to[i], delta); 686 | }); 687 | }, 688 | 689 | start: function(x, y){ 690 | if (!this.check(x, y)) return this; 691 | var scroll = this.element.getScroll(); 692 | return this.parent([scroll.x, scroll.y], [x, y]); 693 | }, 694 | 695 | calculateScroll: function(x, y){ 696 | var element = this.element, 697 | scrollSize = element.getScrollSize(), 698 | scroll = element.getScroll(), 699 | size = element.getSize(), 700 | offset = this.options.offset, 701 | values = {x: x, y: y}; 702 | 703 | for (var z in values){ 704 | if (!values[z] && values[z] !== 0) values[z] = scroll[z]; 705 | if (typeOf(values[z]) != 'number') values[z] = scrollSize[z] - size[z]; 706 | values[z] += offset[z]; 707 | } 708 | 709 | return [values.x, values.y]; 710 | }, 711 | 712 | toTop: function(){ 713 | return this.start.apply(this, this.calculateScroll(false, 0)); 714 | }, 715 | 716 | toLeft: function(){ 717 | return this.start.apply(this, this.calculateScroll(0, false)); 718 | }, 719 | 720 | toRight: function(){ 721 | return this.start.apply(this, this.calculateScroll('right', false)); 722 | }, 723 | 724 | toBottom: function(){ 725 | return this.start.apply(this, this.calculateScroll(false, 'bottom')); 726 | }, 727 | 728 | toElement: function(el, axes){ 729 | axes = axes ? Array.convert(axes) : ['x', 'y']; 730 | var scroll = isBody(this.element) ? {x: 0, y: 0} : this.element.getScroll(); 731 | var position = Object.map(document.id(el).getPosition(this.element), function(value, axis){ 732 | return axes.contains(axis) ? value + scroll[axis] : false; 733 | }); 734 | return this.start.apply(this, this.calculateScroll(position.x, position.y)); 735 | }, 736 | 737 | toElementEdge: function(el, axes, offset){ 738 | axes = axes ? Array.convert(axes) : ['x', 'y']; 739 | el = document.id(el); 740 | var to = {}, 741 | position = el.getPosition(this.element), 742 | size = el.getSize(), 743 | scroll = this.element.getScroll(), 744 | containerSize = this.element.getSize(), 745 | edge = { 746 | x: position.x + size.x, 747 | y: position.y + size.y 748 | }; 749 | 750 | ['x', 'y'].each(function(axis){ 751 | if (axes.contains(axis)){ 752 | if (edge[axis] > scroll[axis] + containerSize[axis]) to[axis] = edge[axis] - containerSize[axis]; 753 | if (position[axis] < scroll[axis]) to[axis] = position[axis]; 754 | } 755 | if (to[axis] == null) to[axis] = scroll[axis]; 756 | if (offset && offset[axis]) to[axis] = to[axis] + offset[axis]; 757 | }, this); 758 | 759 | if (to.x != scroll.x || to.y != scroll.y) this.start(to.x, to.y); 760 | return this; 761 | }, 762 | 763 | toElementCenter: function(el, axes, offset){ 764 | axes = axes ? Array.convert(axes) : ['x', 'y']; 765 | el = document.id(el); 766 | var to = {}, 767 | position = el.getPosition(this.element), 768 | size = el.getSize(), 769 | scroll = this.element.getScroll(), 770 | containerSize = this.element.getSize(); 771 | 772 | ['x', 'y'].each(function(axis){ 773 | if (axes.contains(axis)){ 774 | to[axis] = position[axis] - (containerSize[axis] - size[axis]) / 2; 775 | } 776 | if (to[axis] == null) to[axis] = scroll[axis]; 777 | if (offset && offset[axis]) to[axis] = to[axis] + offset[axis]; 778 | }, this); 779 | 780 | if (to.x != scroll.x || to.y != scroll.y) this.start(to.x, to.y); 781 | return this; 782 | } 783 | 784 | }); 785 | 786 | 787 | 788 | function isBody(element){ 789 | return (/^(?:body|html)$/i).test(element.tagName); 790 | } 791 | 792 | })(); 793 | 794 | /* 795 | --- 796 | 797 | script: Fx.Slide.js 798 | 799 | name: Fx.Slide 800 | 801 | description: Effect to slide an element in and out of view. 802 | 803 | license: MIT-style license 804 | 805 | authors: 806 | - Valerio Proietti 807 | 808 | requires: 809 | - Core/Fx 810 | - Core/Element.Style 811 | - MooTools.More 812 | 813 | provides: [Fx.Slide] 814 | 815 | ... 816 | */ 817 | 818 | Fx.Slide = new Class({ 819 | 820 | Extends: Fx, 821 | 822 | options: { 823 | mode: 'vertical', 824 | wrapper: false, 825 | hideOverflow: true, 826 | resetHeight: false 827 | }, 828 | 829 | initialize: function(element, options){ 830 | element = this.element = this.subject = document.id(element); 831 | this.parent(options); 832 | options = this.options; 833 | 834 | var wrapper = element.retrieve('wrapper'), 835 | styles = element.getStyles('margin', 'position', 'overflow'); 836 | 837 | if (options.hideOverflow) styles = Object.append(styles, {overflow: 'hidden'}); 838 | if (options.wrapper) wrapper = document.id(options.wrapper).setStyles(styles); 839 | 840 | if (!wrapper) wrapper = new Element('div', { 841 | styles: styles 842 | }).wraps(element); 843 | 844 | element.store('wrapper', wrapper).setStyle('margin', 0); 845 | if (element.getStyle('overflow') == 'visible') element.setStyle('overflow', 'hidden'); 846 | 847 | this.now = []; 848 | this.open = true; 849 | this.wrapper = wrapper; 850 | 851 | this.addEvent('complete', function(){ 852 | this.open = (wrapper['offset' + this.layout.capitalize()] != 0); 853 | if (this.open && this.options.resetHeight) wrapper.setStyle('height', ''); 854 | }, true); 855 | }, 856 | 857 | vertical: function(){ 858 | this.margin = 'margin-top'; 859 | this.layout = 'height'; 860 | this.offset = this.element.offsetHeight; 861 | }, 862 | 863 | horizontal: function(){ 864 | this.margin = 'margin-left'; 865 | this.layout = 'width'; 866 | this.offset = this.element.offsetWidth; 867 | }, 868 | 869 | set: function(now){ 870 | this.element.setStyle(this.margin, now[0]); 871 | this.wrapper.setStyle(this.layout, now[1]); 872 | return this; 873 | }, 874 | 875 | compute: function(from, to, delta){ 876 | return [0, 1].map(function(i){ 877 | return Fx.compute(from[i], to[i], delta); 878 | }); 879 | }, 880 | 881 | start: function(how, mode){ 882 | if (!this.check(how, mode)) return this; 883 | this[mode || this.options.mode](); 884 | 885 | var margin = this.element.getStyle(this.margin).toInt(), 886 | layout = this.wrapper.getStyle(this.layout).toInt(), 887 | caseIn = [[margin, layout], [0, this.offset]], 888 | caseOut = [[margin, layout], [-this.offset, 0]], 889 | start; 890 | 891 | switch (how){ 892 | case 'in': start = caseIn; break; 893 | case 'out': start = caseOut; break; 894 | case 'toggle': start = (layout == 0) ? caseIn : caseOut; 895 | } 896 | return this.parent(start[0], start[1]); 897 | }, 898 | 899 | slideIn: function(mode){ 900 | return this.start('in', mode); 901 | }, 902 | 903 | slideOut: function(mode){ 904 | return this.start('out', mode); 905 | }, 906 | 907 | hide: function(mode){ 908 | this[mode || this.options.mode](); 909 | this.open = false; 910 | return this.set([-this.offset, 0]); 911 | }, 912 | 913 | show: function(mode){ 914 | this[mode || this.options.mode](); 915 | this.open = true; 916 | return this.set([0, this.offset]); 917 | }, 918 | 919 | toggle: function(mode){ 920 | return this.start('toggle', mode); 921 | } 922 | 923 | }); 924 | 925 | Element.Properties.slide = { 926 | 927 | set: function(options){ 928 | this.get('slide').cancel().setOptions(options); 929 | return this; 930 | }, 931 | 932 | get: function(){ 933 | var slide = this.retrieve('slide'); 934 | if (!slide){ 935 | slide = new Fx.Slide(this, {link: 'cancel'}); 936 | this.store('slide', slide); 937 | } 938 | return slide; 939 | } 940 | 941 | }; 942 | 943 | Element.implement({ 944 | 945 | slide: function(how, mode){ 946 | how = how || 'toggle'; 947 | var slide = this.get('slide'), toggle; 948 | switch (how){ 949 | case 'hide': slide.hide(mode); break; 950 | case 'show': slide.show(mode); break; 951 | case 'toggle': 952 | var flag = this.retrieve('slide:flag', slide.open); 953 | slide[flag ? 'slideOut' : 'slideIn'](mode); 954 | this.store('slide:flag', !flag); 955 | toggle = true; 956 | break; 957 | default: slide.start(how, mode); 958 | } 959 | if (!toggle) this.eliminate('slide:flag'); 960 | return this; 961 | } 962 | 963 | }); 964 | 965 | /* 966 | --- 967 | 968 | script: Keyboard.js 969 | 970 | name: Keyboard 971 | 972 | description: KeyboardEvents used to intercept events on a class for keyboard and format modifiers in a specific order so as to make alt+shift+c the same as shift+alt+c. 973 | 974 | license: MIT-style license 975 | 976 | authors: 977 | - Perrin Westrich 978 | - Aaron Newton 979 | - Scott Kyle 980 | 981 | requires: 982 | - Core/Events 983 | - Core/Options 984 | - Core/Element.Event 985 | - Element.Event.Pseudos.Keys 986 | 987 | provides: [Keyboard] 988 | 989 | ... 990 | */ 991 | 992 | (function(){ 993 | 994 | var Keyboard = this.Keyboard = new Class({ 995 | 996 | Extends: Events, 997 | 998 | Implements: [Options], 999 | 1000 | options: {/* 1001 | onActivate: function(){}, 1002 | onDeactivate: function(){},*/ 1003 | defaultEventType: 'keydown', 1004 | active: false, 1005 | manager: null, 1006 | events: {}, 1007 | nonParsedEvents: ['activate', 'deactivate', 'onactivate', 'ondeactivate', 'changed', 'onchanged'] 1008 | }, 1009 | 1010 | initialize: function(options){ 1011 | if (options && options.manager){ 1012 | this._manager = options.manager; 1013 | delete options.manager; 1014 | } 1015 | this.setOptions(options); 1016 | this._setup(); 1017 | }, 1018 | 1019 | addEvent: function(type, fn, internal){ 1020 | return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn, internal); 1021 | }, 1022 | 1023 | removeEvent: function(type, fn){ 1024 | return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn); 1025 | }, 1026 | 1027 | toggleActive: function(){ 1028 | return this[this.isActive() ? 'deactivate' : 'activate'](); 1029 | }, 1030 | 1031 | activate: function(instance){ 1032 | if (instance){ 1033 | if (instance.isActive()) return this; 1034 | //if we're stealing focus, store the last keyboard to have it so the relinquish command works 1035 | if (this._activeKB && instance != this._activeKB){ 1036 | this.previous = this._activeKB; 1037 | this.previous.fireEvent('deactivate'); 1038 | } 1039 | //if we're enabling a child, assign it so that events are now passed to it 1040 | this._activeKB = instance.fireEvent('activate'); 1041 | Keyboard.manager.fireEvent('changed'); 1042 | } else if (this._manager){ 1043 | //else we're enabling ourselves, we must ask our parent to do it for us 1044 | this._manager.activate(this); 1045 | } 1046 | return this; 1047 | }, 1048 | 1049 | isActive: function(){ 1050 | return this._manager ? (this._manager._activeKB == this) : (Keyboard.manager == this); 1051 | }, 1052 | 1053 | deactivate: function(instance){ 1054 | if (instance){ 1055 | if (instance === this._activeKB){ 1056 | this._activeKB = null; 1057 | instance.fireEvent('deactivate'); 1058 | Keyboard.manager.fireEvent('changed'); 1059 | } 1060 | } else if (this._manager){ 1061 | this._manager.deactivate(this); 1062 | } 1063 | return this; 1064 | }, 1065 | 1066 | relinquish: function(){ 1067 | if (this.isActive() && this._manager && this._manager.previous) this._manager.activate(this._manager.previous); 1068 | else this.deactivate(); 1069 | return this; 1070 | }, 1071 | 1072 | //management logic 1073 | manage: function(instance){ 1074 | if (instance._manager) instance._manager.drop(instance); 1075 | this._instances.push(instance); 1076 | instance._manager = this; 1077 | if (!this._activeKB) this.activate(instance); 1078 | return this; 1079 | }, 1080 | 1081 | drop: function(instance){ 1082 | instance.relinquish(); 1083 | this._instances.erase(instance); 1084 | if (this._activeKB == instance){ 1085 | if (this.previous && this._instances.contains(this.previous)) this.activate(this.previous); 1086 | else this._activeKB = this._instances[0]; 1087 | } 1088 | return this; 1089 | }, 1090 | 1091 | trace: function(){ 1092 | Keyboard.trace(this); 1093 | }, 1094 | 1095 | each: function(fn){ 1096 | Keyboard.each(this, fn); 1097 | }, 1098 | 1099 | /* 1100 | PRIVATE METHODS 1101 | */ 1102 | 1103 | _instances: [], 1104 | 1105 | _disable: function(instance){ 1106 | if (this._activeKB == instance) this._activeKB = null; 1107 | }, 1108 | 1109 | _setup: function(){ 1110 | this.addEvents(this.options.events); 1111 | //if this is the root manager, nothing manages it 1112 | if (Keyboard.manager && !this._manager) Keyboard.manager.manage(this); 1113 | if (this.options.active) this.activate(); 1114 | else this.relinquish(); 1115 | }, 1116 | 1117 | _handle: function(event, type){ 1118 | //Keyboard.stop(event) prevents key propagation 1119 | if (event.preventKeyboardPropagation) return; 1120 | 1121 | var bubbles = !!this._manager; 1122 | if (bubbles && this._activeKB){ 1123 | this._activeKB._handle(event, type); 1124 | if (event.preventKeyboardPropagation) return; 1125 | } 1126 | this.fireEvent(type, event); 1127 | 1128 | if (!bubbles && this._activeKB) this._activeKB._handle(event, type); 1129 | } 1130 | 1131 | }); 1132 | 1133 | var parsed = {}; 1134 | var modifiers = ['shift', 'control', 'alt', 'meta']; 1135 | var regex = /^(?:shift|control|ctrl|alt|meta)$/; 1136 | 1137 | Keyboard.parse = function(type, eventType, ignore){ 1138 | if (ignore && ignore.contains(type.toLowerCase())) return type; 1139 | 1140 | type = type.toLowerCase().replace(/^(keyup|keydown):/, function($0, $1){ 1141 | eventType = $1; 1142 | return ''; 1143 | }); 1144 | 1145 | if (!parsed[type]){ 1146 | if (type != '+'){ 1147 | var key, mods = {}; 1148 | type.split('+').each(function(part){ 1149 | if (regex.test(part)) mods[part] = true; 1150 | else key = part; 1151 | }); 1152 | 1153 | mods.control = mods.control || mods.ctrl; // allow both control and ctrl 1154 | 1155 | var keys = []; 1156 | modifiers.each(function(mod){ 1157 | if (mods[mod]) keys.push(mod); 1158 | }); 1159 | 1160 | if (key) keys.push(key); 1161 | parsed[type] = keys.join('+'); 1162 | } else { 1163 | parsed[type] = type; 1164 | } 1165 | } 1166 | 1167 | return eventType + ':keys(' + parsed[type] + ')'; 1168 | }; 1169 | 1170 | Keyboard.each = function(keyboard, fn){ 1171 | var current = keyboard || Keyboard.manager; 1172 | while (current){ 1173 | fn(current); 1174 | current = current._activeKB; 1175 | } 1176 | }; 1177 | 1178 | Keyboard.stop = function(event){ 1179 | event.preventKeyboardPropagation = true; 1180 | }; 1181 | 1182 | Keyboard.manager = new Keyboard({ 1183 | active: true 1184 | }); 1185 | 1186 | Keyboard.trace = function(keyboard){ 1187 | keyboard = keyboard || Keyboard.manager; 1188 | var hasConsole = window.console && console.log; 1189 | if (hasConsole) console.log('the following items have focus: '); 1190 | Keyboard.each(keyboard, function(current){ 1191 | if (hasConsole) console.log(document.id(current.widget) || current.wiget || current); 1192 | }); 1193 | }; 1194 | 1195 | var handler = function(event){ 1196 | var keys = []; 1197 | modifiers.each(function(mod){ 1198 | if (event[mod]) keys.push(mod); 1199 | }); 1200 | 1201 | if (!regex.test(event.key)) keys.push(event.key); 1202 | Keyboard.manager._handle(event, event.type + ':keys(' + keys.join('+') + ')'); 1203 | }; 1204 | 1205 | document.addEvents({ 1206 | 'keyup': handler, 1207 | 'keydown': handler 1208 | }); 1209 | 1210 | })(); 1211 | 1212 | /* 1213 | --- 1214 | 1215 | script: Color.js 1216 | 1217 | name: Color 1218 | 1219 | description: Class for creating and manipulating colors in JavaScript. Supports HSB -> RGB Conversions and vice versa. 1220 | 1221 | license: MIT-style license 1222 | 1223 | authors: 1224 | - Valerio Proietti 1225 | 1226 | requires: 1227 | - Core/Array 1228 | - Core/String 1229 | - Core/Number 1230 | - Core/Hash 1231 | - Core/Function 1232 | - MooTools.More 1233 | 1234 | provides: [Color] 1235 | 1236 | ... 1237 | */ 1238 | 1239 | (function(){ 1240 | 1241 | var Color = this.Color = new Type('Color', function(color, type){ 1242 | if (arguments.length >= 3){ 1243 | type = 'rgb'; color = Array.slice(arguments, 0, 3); 1244 | } else if (typeof color == 'string'){ 1245 | if (color.match(/rgb/)) color = color.rgbToHex().hexToRgb(true); 1246 | else if (color.match(/hsb/)) color = color.hsbToRgb(); 1247 | else color = color.hexToRgb(true); 1248 | } 1249 | type = type || 'rgb'; 1250 | switch (type){ 1251 | case 'hsb': 1252 | var old = color; 1253 | color = color.hsbToRgb(); 1254 | color.hsb = old; 1255 | break; 1256 | case 'hex': color = color.hexToRgb(true); break; 1257 | } 1258 | color.rgb = color.slice(0, 3); 1259 | color.hsb = color.hsb || color.rgbToHsb(); 1260 | color.hex = color.rgbToHex(); 1261 | return Object.append(color, this); 1262 | }); 1263 | 1264 | Color.implement({ 1265 | 1266 | mix: function(){ 1267 | var colors = Array.slice(arguments); 1268 | var alpha = (typeOf(colors.getLast()) == 'number') ? colors.pop() : 50; 1269 | var rgb = this.slice(); 1270 | colors.each(function(color){ 1271 | color = new Color(color); 1272 | for (var i = 0; i < 3; i++) rgb[i] = Math.round((rgb[i] / 100 * (100 - alpha)) + (color[i] / 100 * alpha)); 1273 | }); 1274 | return new Color(rgb, 'rgb'); 1275 | }, 1276 | 1277 | invert: function(){ 1278 | return new Color(this.map(function(value){ 1279 | return 255 - value; 1280 | })); 1281 | }, 1282 | 1283 | setHue: function(value){ 1284 | return new Color([value, this.hsb[1], this.hsb[2]], 'hsb'); 1285 | }, 1286 | 1287 | setSaturation: function(percent){ 1288 | return new Color([this.hsb[0], percent, this.hsb[2]], 'hsb'); 1289 | }, 1290 | 1291 | setBrightness: function(percent){ 1292 | return new Color([this.hsb[0], this.hsb[1], percent], 'hsb'); 1293 | } 1294 | 1295 | }); 1296 | 1297 | this.$RGB = function(r, g, b){ 1298 | return new Color([r, g, b], 'rgb'); 1299 | }; 1300 | 1301 | this.$HSB = function(h, s, b){ 1302 | return new Color([h, s, b], 'hsb'); 1303 | }; 1304 | 1305 | this.$HEX = function(hex){ 1306 | return new Color(hex, 'hex'); 1307 | }; 1308 | 1309 | Array.implement({ 1310 | 1311 | rgbToHsb: function(){ 1312 | var red = this[0], 1313 | green = this[1], 1314 | blue = this[2], 1315 | hue = 0, 1316 | max = Math.max(red, green, blue), 1317 | min = Math.min(red, green, blue), 1318 | delta = max - min, 1319 | brightness = max / 255, 1320 | saturation = (max != 0) ? delta / max : 0; 1321 | 1322 | if (saturation != 0){ 1323 | var rr = (max - red) / delta; 1324 | var gr = (max - green) / delta; 1325 | var br = (max - blue) / delta; 1326 | if (red == max) hue = br - gr; 1327 | else if (green == max) hue = 2 + rr - br; 1328 | else hue = 4 + gr - rr; 1329 | hue /= 6; 1330 | if (hue < 0) hue++; 1331 | } 1332 | return [Math.round(hue * 360), Math.round(saturation * 100), Math.round(brightness * 100)]; 1333 | }, 1334 | 1335 | hsbToRgb: function(){ 1336 | var br = Math.round(this[2] / 100 * 255); 1337 | if (this[1] == 0){ 1338 | return [br, br, br]; 1339 | } else { 1340 | var hue = this[0] % 360; 1341 | var f = hue % 60; 1342 | var p = Math.round((this[2] * (100 - this[1])) / 10000 * 255); 1343 | var q = Math.round((this[2] * (6000 - this[1] * f)) / 600000 * 255); 1344 | var t = Math.round((this[2] * (6000 - this[1] * (60 - f))) / 600000 * 255); 1345 | switch (Math.floor(hue / 60)){ 1346 | case 0: return [br, t, p]; 1347 | case 1: return [q, br, p]; 1348 | case 2: return [p, br, t]; 1349 | case 3: return [p, q, br]; 1350 | case 4: return [t, p, br]; 1351 | case 5: return [br, p, q]; 1352 | } 1353 | } 1354 | return false; 1355 | } 1356 | 1357 | }); 1358 | 1359 | String.implement({ 1360 | 1361 | rgbToHsb: function(){ 1362 | var rgb = this.match(/\d{1,3}/g); 1363 | return (rgb) ? rgb.rgbToHsb() : null; 1364 | }, 1365 | 1366 | hsbToRgb: function(){ 1367 | var hsb = this.match(/\d{1,3}/g); 1368 | return (hsb) ? hsb.hsbToRgb() : null; 1369 | } 1370 | 1371 | }); 1372 | 1373 | })(); 1374 | 1375 | -------------------------------------------------------------------------------- /js/sexhr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sexhr.js 3 | * 4 | * A simple but useful promise-enabled wrapper around XHR. Takes care of a lot 5 | * of common tasks without abstracting away any power or freedom. 6 | * ----------------------------------------------------------------------------- 7 | * 8 | * Copyright (c) 2015, Lyon Bros Enterprises, LLC. (http://www.lyonbros.com) 9 | * 10 | * Licensed under The MIT License. 11 | * Redistributions of files must retain the above copyright notice. 12 | */ 13 | 14 | (function() { 15 | "use strict"; 16 | this.Sexhr = function(options) 17 | { 18 | options || (options = {}); 19 | 20 | return new Promise(function(resolve, reject) { 21 | var url = options.url; 22 | var method = (options.method || 'get').toUpperCase(); 23 | var emulate = options.emulate || true; 24 | 25 | if(!options.url) throw new Error('no url given'); 26 | 27 | url = url.replace(/#.*$/, ''); 28 | var qs = []; 29 | if(options.querydata) 30 | { 31 | qs = Object.keys(options.querydata) 32 | .map(function(key) { 33 | return key + '=' + encodeURIComponent(options.querydata[key]); 34 | }); 35 | } 36 | if(emulate && ['GET', 'POST'].indexOf(method) < 0) 37 | { 38 | qs.push('_method='+method); 39 | method = 'POST'; 40 | } 41 | if(qs.length) 42 | { 43 | var querystring = qs.join('&'); 44 | if(url.match(/\?/)) 45 | { 46 | url = url.replace(/&$/) + '&' + querystring; 47 | } 48 | else 49 | { 50 | url += '?' + querystring; 51 | } 52 | } 53 | 54 | var xhr = new XMLHttpRequest(); 55 | xhr.open(method, url, true); 56 | xhr.responseType = options.response_type || ''; 57 | if(options.timeout) xhr.timeout = options.timeout; 58 | 59 | Object.keys(options.headers || {}).forEach(function(k) { 60 | xhr.setRequestHeader(k, options.headers[k]); 61 | }); 62 | xhr.onload = function(e) 63 | { 64 | if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) 65 | { 66 | var value = xhr.response; 67 | return resolve([value, xhr]); 68 | } 69 | else if(xhr.status >= 400) 70 | { 71 | reject({xhr: xhr, code: xhr.status, msg: xhr.response}); 72 | } 73 | }; 74 | xhr.onabort = function(e) 75 | { 76 | reject({xhr: xhr, code: -2, msg: 'aborted'}); 77 | }; 78 | xhr.onerror = function(e) 79 | { 80 | reject({xhr: xhr, code: -1, msg: 'error'}); 81 | }; 82 | xhr.ontimeout = function(e) 83 | { 84 | reject({xhr: xhr, code: -3, msg: 'timeout'}); 85 | }; 86 | 87 | // set xhr.on[progress|abort|etc] 88 | Object.keys(options).forEach(function(k) { 89 | if(k.substr(0, 2) != 'on') return false; 90 | if(['onload', 'onerror', 'onabort', 'ontimeout'].indexOf(k) >= 0) return false; 91 | var fn = options[k]; 92 | xhr[k] = function(e) { fn(e, xhr); }; 93 | }); 94 | // set xhr.upload.on[progress|abort|etc] 95 | Object.keys(options.upload || {}).forEach(function(k) { 96 | if(k.substr(0, 2) != 'on') return false; 97 | var fn = options[k]; 98 | xhr.upload[k] = function(e) { fn(e, xhr); }; 99 | }); 100 | 101 | if(options.override) options.override(xhr); 102 | 103 | xhr.send(options.data); 104 | }); 105 | }; 106 | }).apply((typeof exports != 'undefined') ? exports : this); 107 | 108 | -------------------------------------------------------------------------------- /js/stripe.js: -------------------------------------------------------------------------------- 1 | const load_modal_template = function(modal, template_data) { 2 | template_data || (template_data = {}); 3 | const get_modals = function() { return document.body.getElements('.active-payment-modal'); }; 4 | get_modals().forEach(function(el) { 5 | if(el.getStyle('display') == 'none') { 6 | el.destroy(); 7 | } 8 | }); 9 | 10 | var copy = modal.clone() 11 | .addClass('active-payment-modal'); 12 | copy.set('html', copy.get('html').replace(/\[\[([a-z0-9_-]+)\]\]/gi, function(_, match) { 13 | return template_data[match] || ''; 14 | })); 15 | var active_modal = get_modals()[0]; 16 | if(active_modal && active_modal.getStyle('display') != 'none') { 17 | active_modal.set('html', copy.get('html')); 18 | } else { 19 | copy.addEvent('click:relay(.close-modal)', function(e) { 20 | get_modals().map(function(el) { 21 | UIkit.modal(el).hide(); 22 | el.destroy(); 23 | }); 24 | }); 25 | UIkit.modal(copy).show(); 26 | } 27 | }; 28 | 29 | const stripe_success = function(res) { 30 | if(!res) return; 31 | load_modal_template($('payment-success-modal'), res); 32 | }; 33 | 34 | const stripe_error = function(errobj) { 35 | var errormsg = errobj.msg; 36 | try { var jsonerr = JSON.parse(errormsg); } catch(_) {} 37 | var msg = jsonerr ? jsonerr.error.message : errormsg; 38 | var tpl = { 39 | error: msg, 40 | } 41 | load_modal_template($('payment-error-modal'), tpl); 42 | }; 43 | 44 | const stripe_handler = function(btnsel, type) { 45 | const payment_types = { 46 | 'premium': { 47 | title: 'Turtl Premium ($3/mo)', 48 | button: 'Get Premium', 49 | amount: 300, 50 | }, 51 | 'business': { 52 | title: 'Turtl Business ($8/mo)', 53 | button: 'Get Business', 54 | amount: 800, 55 | }, 56 | }; 57 | const payment_title = payment_types[type].title; 58 | const button_txt = payment_types[type].button; 59 | const amount = payment_types[type].amount; 60 | 61 | var handler = StripeCheckout.configure({ 62 | key: stripe_pubkey, 63 | image: "/images/logo.svg", 64 | name: "Turtl", 65 | description: payment_title, 66 | amount: amount, 67 | zipCode: true, 68 | panelLabel: button_txt, 69 | allowRememberMe: false, 70 | token: function(tokenobj) { 71 | var url = window.location.toString().match(/turtl\.loc/) ? 72 | 'http://api.turtl.loc:8181/payment' : 73 | 'https://apiv3.turtlapp.com/payment'; 74 | var paymentinfo = { 75 | type: type, 76 | token: tokenobj, 77 | }; 78 | var req = { 79 | url: url, 80 | method: 'POST', 81 | data: JSON.stringify(paymentinfo), 82 | headers: { 83 | 'Content-Type': 'application/json', 84 | }, 85 | }; 86 | load_modal_template($('payment-loading-modal')); 87 | Sexhr(req) 88 | .spread(function(res, xhr) { 89 | _paq.push(['trackGoal', 4, amount / 100]); 90 | stripe_success(JSON.parse(res)); 91 | }) 92 | .catch(function(err) { 93 | stripe_error(err); 94 | }); 95 | }, 96 | }); 97 | 98 | document.getElement(btnsel).addEventListener('click', function(e) { 99 | e.preventDefault(); 100 | _paq.push(['trackGoal', 1, amount / 100]); 101 | handler.open(); 102 | }); 103 | }; 104 | 105 | -------------------------------------------------------------------------------- /keybase.txt: -------------------------------------------------------------------------------- 1 | ================================================================== 2 | https://keybase.io/orthecreedence 3 | -------------------------------------------------------------------- 4 | 5 | I hereby claim: 6 | 7 | * I am an admin of https://turtlapp.com 8 | * I am orthecreedence (https://keybase.io/orthecreedence) on keybase. 9 | * I have a public key with fingerprint DEDF 113E 5424 8344 1637 16B5 5C66 FAD1 3222 D757 10 | 11 | To do so, I am signing this object: 12 | 13 | { 14 | "body": { 15 | "key": { 16 | "eldest_kid": "010162d53a2e14a912d94d7c22e87e8592c1f0264b4a6b75cf35c4b23b4d104d0e370a", 17 | "fingerprint": "dedf113e54248344163716b55c66fad13222d757", 18 | "host": "keybase.io", 19 | "key_id": "5c66fad13222d757", 20 | "kid": "010162d53a2e14a912d94d7c22e87e8592c1f0264b4a6b75cf35c4b23b4d104d0e370a", 21 | "uid": "c51221b7dbba12757a50d885987c9b00", 22 | "username": "orthecreedence" 23 | }, 24 | "revoke": { 25 | "sig_ids": [ 26 | "e7935869ac7fec3fa62e38442f2e0f13dcd619fc4d136950177ada38e09cdd180f" 27 | ] 28 | }, 29 | "service": { 30 | "hostname": "turtlapp.com", 31 | "protocol": "https:" 32 | }, 33 | "type": "web_service_binding", 34 | "version": 1 35 | }, 36 | "ctime": 1472855194, 37 | "expire_in": 157680000, 38 | "prev": "22edd66b47aecfbdcf4335a59ab071d0ea37bd732a7e123e62a53ae418a9bd46", 39 | "seqno": 19, 40 | "tag": "signature" 41 | } 42 | 43 | which yields the signature: 44 | 45 | -----BEGIN PGP MESSAGE----- 46 | Version: GnuPG v1 47 | 48 | owGtkntQVFUcxxcMkUdAqLwiB2/UH7XAPfd9t4cpijaaQqNuJQL33nPuclnYXXaX 49 | RYZWISfRQQMSAydqwkoCgT+iIUcCfPAIMkaMGKnBSSAaw4AiBfExnWXsv/7s/HPm 50 | nPP9fX7f8z2n/PFlOn8v48BrNdeuur7y+u6mrDP23W8qJGQrLCAMhYQZLU0oGyKH 51 | M92sQcJAkIAEHAVZWqIQYCQRUFBkIK9QFBJ4JLAipQCVpDhGZiRO5llFpVmFkSla 52 | ZiAgGUgimiclQk+omsWE7Da7ZnFiLERQBYBGLEMxAs0wgKN5wMksq3CcKkFAUxQF 53 | eZbHhZlWh6cCm5MlB4rXrHgPL9KX7P2H/n/2nbeEU1hAUUDmoSxLgMKNJJaEAsYI 54 | vCLKJOkROpDdIuUgrLbanZlIsSMEkUVBhFtP2JHLakaedB2aCVt3EIY9BOJFmhU4 55 | UVJ4FSm0KnEUogWGoVQKkSqgoQI5IKoKdkRzIksCnpegRAuIFBUIgUCqxF7Mxn1d 56 | mrIE92T1yIMzz+7Mlmy2eMWag93Z7FanVbFm45NMp9PmMGBXbYSzwObR5iM5/REl 57 | XdYsED8VLnEhu0OzWggDwE0Up+bBAoanBJYFIqMn0D6bZkfpmkfB8pxA4uFphFwY 58 | iXOGkONkhpeQospQURmaZiVWlGSSBzheieZlyNOUxCNA0YijJPxWiAGCJMqQ4QjP 59 | vXItVswW9YRTMmEmjs4i4XvhRP2PeEc/pvPy1y338fZ8Yp2/X8i/P/t5U/A9nxrb 60 | zOaDD25SxzIWKzYFRu6FTQ93dWWMBSWmPfNsx47CYub4N+c6d7pd4fWxw7UxAcp6 61 | +b2p6o6S0trGgZnZ/hudE75HTw5WMFFBZ35YF/76htbPsyoS6/Z5511IDstT2T15 62 | Cbq048sesOH39u+/i34NsqWey61LJkpqCjqmzOaXDuRu/6BfF7Dx65OpbatzY4dG 63 | j4za7CW/fH+0JqqyJSKy6q0tLQ3PuS+fmn359ibL5QrAhrzZM9/XsK3XEhZj2tLl 64 | 4/aN9B17Yrzst1cc1ZnJh6787juq3M6KWzU7AXpaa3dH7XI1L3/f+NTQaq+WDfM9 65 | N8ZS2ET9wbNZYb25jcGnSx2tcULPUENuzNsZG0Oj1eYvEorakyaDRxL6hk9PmXeG 66 | 3o37cfjOJXnur4/euDZtrNp+KzPi497FxrWTJ54c/2OSezUuR2yLryxbcb84x+8z 67 | w9S4Lu2TWyEhK5l3uo2Hhv6Uo+ruDPqFfvriietFxWDdbHlixqqEF7oKCkdSjFXO 68 | d8uabBcjprpt7pGiNWFXomIDrANp44uV9Ws+PNVzYMfZUN/5mYXdMwvTSQ9Dq8kz 69 | 8Hxq2dWfzn95ODrQYM8/Flk/nPM37JprrihxbzZNTKzsDM5L0Bc8/e1I+bYVPwey 70 | I/2a1/X01pT1jgU/sXS2pfXSdHu7Y23fVu3CYGHr1vzupIG5g9NtF8P+AQ== 71 | =j2tc 72 | -----END PGP MESSAGE----- 73 | 74 | And finally, I am proving ownership of this host by posting or 75 | appending to this document. 76 | 77 | View my publicly-auditable identity here: https://keybase.io/orthecreedence 78 | 79 | ================================================================== 80 | -------------------------------------------------------------------------------- /manage/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Manage your subscription" 4 | permalink: "manage/" 5 | body_class: 'manage' 6 | --- 7 | 8 |

    9 |
    10 |
    11 |

    Manage your subscription

    12 | 13 |
    14 | 15 |
    16 |

    17 | Your billing email is used to contact you about your payment 18 | and your account. Your Turtl account email is used to link 19 | your billing info to your Turtl account so we know which 20 | account to give subscription features to. 21 |

    22 |
    23 |
    24 | 25 | 26 |
    27 |
    28 | 29 |
    30 | 31 | 32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 | 42 |
    43 |
    44 |
    45 |
    46 | 47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turtl-www", 3 | "version": "0.6.0", 4 | "description": "Turtl's website", 5 | "author": "Andrew Lyon", 6 | "license": "GPLv3", 7 | "devDependencies": { 8 | "autoprefixer": "^6.0.3", 9 | "es6-promise": "^3.0.2", 10 | "less": "^2.5.3", 11 | "postcss-cli": "^2.3.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /premium/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Premium" 4 | permalink: "premium/" 5 | --- 6 | 7 | {% if site.enable_premium %} 8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    Premium
    15 |

    $3/mo

    16 |
      17 |
    • Store up to 10GB of note data
    • 18 |
    • Collaborate with up to 10 people on each space
    • 19 |
    • Community support
    • 20 |
    21 |
    22 | Start 30 day trial 23 | 24 | 25 |
    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 |
    Basic
    34 |

    Free

    35 |
      36 |
    • Store up to 50MB of note data
    • 37 |
    • Collaborate with up to three people on each space
    • 38 |
    • Community support
    • 39 |
    40 |
    41 | Download Turtl 42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    Business
    50 |

    $8/mo

    51 |
      52 |
    • Store up to 50GB of note data
    • 53 |
    • Collaborate with up to 50 people on each space
    • 54 |
    • Dedicated support
    • 55 |
    56 |
    57 | Start 30 day trial 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 |
    FeaturesBasicPremiumBusiness
    Encrypt and secure all your private data
    Use Turtl on unlimited devices
    Collaboraters per Space32050
    Storage50MB10GB50GB
    SupportCommunityCommunityDedicated
    CostFree$3/mo$8/mo
    116 | 117 |
    118 |

    Custom plans

    119 |

    120 | Need something more than what we offer above? Get in touch and we'll figure out a plan for you! 121 |

    122 |
    123 |
    124 |
    125 | 126 |
    127 |
    128 |
    129 |
    130 |
    131 |
    132 |
    133 | 134 |
    135 |
    136 |
    137 |

    Thank you!

    138 |

    139 | Your payment information was successfully processed. 140 |

    141 |

    142 | Please save this link, which will allow you to manage your Turtl 143 | subscription: 144 |

    145 |
    [[manage_url]]
    146 |

    147 | This link has also been emailed to you at [[email]]! 148 |

    149 |
    150 | 151 |
    152 |
    153 |
    154 |
    155 | 156 |
    157 |
    158 |
    159 |

    There was a problem with your payment

    160 |

    161 | Unfortunately, it looks like there was a problem processing your 162 | payment: 163 |

    164 |
    [[error]]
    165 |

    Please try again, or email us at support@turtlapp.com for help.

    166 |
    167 | 168 |
    169 |
    170 |
    171 |
    172 | {% else %} 173 |
    174 |
    175 |
    176 |

    Turtl Premium

    177 |

    178 | The Turtl service is free and in most cases should work for most users. 179 | However, beginning soon, Turtl will launch a Premium service that will 180 | include more storage and will unlock various features in the Turtl apps. 181 |

    182 |

    183 | In the meantime while we figure out what our Premium service will offer, 184 | please consider making a donation to aid in Turtl's development! 185 |

    186 |

    187 | If you want the total freedom and security of Turtl without paying us, 188 | you are more than free to run your own Turtl server. 189 |

    190 |
    191 |
    192 |
    193 | {% endif %} 194 | 195 | -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Privacy" 4 | permalink: "privacy/" 5 | --- 6 | 7 | Last update: __December 8, 2015__ 8 | 9 | Privacy 10 | ======= 11 | 12 | We have created this page to explain, very clearly and without legal speak, how 13 | we collect and use your information while the Turtl app is connected to our 14 | servers. 15 | 16 | Note that this document will update as new features are added to Turtl. However, 17 | parts of this document that state that something will *never* happen will not be 18 | changed in any material way other than to clarify any possible ambiguity. When 19 | we say never, we mean it. 20 | 21 | ## Information collection 22 | 23 | Times when Turtl collects user information: 24 | 25 | - When users join Turtl. They do so with a username and password: the username 26 | is currently never sent to the servers, but in future versions may be sent in 27 | plain text to our servers. The password, along with the username, create two 28 | pieces of data: an authentication token used to verify that the user logging 29 | in is the same user we have on our servers, and a master key used to encrypt 30 | and decrypt your profile. The master key is never sent outside of the Turtl 31 | app. The authentication token is created by one-way hashing the username and 32 | password and then encrypting using the master key. It does not contain any 33 | personally identifiable information, nor can any be reversed from the token. 34 | - When users add a persona. Personas allow your account, which is private by 35 | default, to be shared with by other people. So when you create a persona, 36 | Turtl asks for an email address (required), name (optional), and in the future 37 | possibly more optional fields (such as an avatar photo). 38 | - When you add data to your profile. The notes/bookmarks/files you add will all 39 | be encrypted (in the client) then sent to the Turtl service (where all your 40 | data remains encrypted). Your data is only ever decrypted when inside the 41 | application, and the only way to decrypt your profile is with your master key 42 | or if you purposefuly share with another person. Your profile cannot even be 43 | read by us. 44 | 45 | ## Information sharing/disclosure 46 | 47 | Times when Turtl shares your information: 48 | 49 | - When you successfully log in to your account, Turtl will download your data 50 | profile (notes/bookmarks/files) to your local computer, in encrypted form, 51 | and then decrypt your data when it arrives. 52 | - When someone is searching for your email in the sharing interface, Turtl will 53 | indicate whether or not the email typed in belongs to a current Turtl persona. 54 | However, one must be logged in to the Turtl service to perform this search, 55 | and there is no way to search multiple emails based on a wildcard. An email 56 | must be known beforehand. 57 | - When United States law enforcement or a government agency has a court order 58 | or warrant which mandates that we share information on a specific user, we 59 | must comply. However, keep in mind that your data profile remains encrypted 60 | during this process. We have no way to decrypt your data, and most likely 61 | neither does any law enforcement agency. Whether or not you choose to share 62 | your encryption key with them is your decision. 63 | 64 | The above may change, but with the restriction that Turtl will never share your 65 | information with any third party other than a law enforcement agency which has 66 | a valid, legal court order mandating us to turn over information. We will not 67 | give your information to third parties. 68 | 69 | ## Advertisement 70 | 71 | Turtl has no third-party advertisements of any kind. Turtl will never have any 72 | form of third-party advertisement. Doing so would cripple the security measures 73 | Turtl takes to protect your information. 74 | 75 | At times, Turtl may advertise, endorse, or promote features or other products, 76 | but at no time will any third-party advertiser be allowed to place any hooks 77 | into our codebase. Any advertisements run on the Turtl application will be run 78 | by Turtl and no one else. 79 | 80 | -------------------------------------------------------------------------------- /releases/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | 4 | -------------------------------------------------------------------------------- /users/confirm/error.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Error confirming your account" 4 | permalink: "users/confirm/error/" 5 | --- 6 | 7 | # There was a problem confirming your account 8 | 9 | It looks like we ran into an error while confirming your account. It may be that 10 | your account has already been confirmed, or you used the link from an old 11 | confirmation email (a new token is generated each time you request your 12 | confirmation email). 13 | 14 | If the problem persists, [please get in touch](/contact). 15 | 16 | -------------------------------------------------------------------------------- /users/confirm/success.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Confirmation succeeded" 4 | permalink: "users/confirm/success/" 5 | --- 6 | 7 | # Confirmation successful! 8 | 9 | Your email is now confirmed. This means you can share your Spaces with others 10 | and they can share with you. 11 | 12 | Enjoy! (You can close this tab) 13 | 14 | -------------------------------------------------------------------------------- /users/delete/error.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Error deleting your account" 4 | permalink: "users/delete/error/" 5 | --- 6 | 7 | # There was a problem deleting your account 8 | 9 | It looks like we ran into an error while confirming your account. It may be that 10 | your account has already been delete, or you used the link from an old 11 | deletion request email. 12 | 13 | Please try the [delete request](/users/delete) again! 14 | 15 | If the problem persists, [get in touch](/contact). 16 | 17 | -------------------------------------------------------------------------------- /users/delete/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Delete your Turtl account" 4 | permalink: "users/delete/" 5 | --- 6 | 7 |

    Delete your Turtl account

    8 | 9 |

    10 | Lost your password? Want to sign up again with your email address? 11 |

    12 | 13 |

    14 | You came to the right place! Enter your email here and we'll send you an email 15 | with a link in it that will let you remove your account. 16 |

    17 | 18 |

    19 | Beware! If you complete this process, all the data in your account will be lost! 20 |

    21 | 22 |
    23 |
    24 |
    25 |
    26 | 27 |
    28 | 29 |
    30 |
    31 | 32 | 37 | 38 | -------------------------------------------------------------------------------- /users/delete/success.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Account deletion succeeded" 4 | permalink: "users/delete/success/" 5 | --- 6 | 7 | # Account deletion successful! 8 | 9 | Your Turtl account has been removed. 10 | 11 | --------------------------------------------------------------------------------