├── .codeclimate.yml ├── .gitchangelog.rc ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE.rst ├── README.rst ├── images ├── rvo.png └── screenshot.png ├── release.sh ├── rvo ├── __init__.py ├── analysis.py ├── cli.py ├── commands │ ├── __init__.py │ ├── add.py │ ├── append.py │ ├── delete.py │ ├── edit.py │ ├── export.py │ ├── info.py │ ├── list.py │ ├── log.py │ ├── mail.py │ ├── memories.py │ ├── modify.py │ ├── ping.py │ ├── rimport.py │ ├── show.py │ └── stats.py ├── config.py ├── crypto.py ├── db.py ├── transaction.py ├── utils.py ├── validate.py └── views.py ├── setup.py └── tests ├── conftest.py ├── test_add.py ├── test_append.py ├── test_delete.py ├── test_edit.py ├── test_export.py ├── test_import.py ├── test_info.py ├── test_list.py ├── test_log.py ├── test_memories.py ├── test_modify.py ├── test_show.py ├── test_stats.py └── test_validate.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | fixme: 12 | enabled: true 13 | radon: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.inc" 18 | - "**.js" 19 | - "**.jsx" 20 | - "**.module" 21 | - "**.php" 22 | - "**.py" 23 | - "**.rb" 24 | exclude_paths: 25 | - tests/ 26 | -------------------------------------------------------------------------------- /.gitchangelog.rc: -------------------------------------------------------------------------------- 1 | ## 2 | ## Format 3 | ## 4 | ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] 5 | ## 6 | ## Description 7 | ## 8 | ## ACTION is one of 'chg', 'fix', 'new' 9 | ## 10 | ## Is WHAT the change is about. 11 | ## 12 | ## 'chg' is for refactor, small improvement, cosmetic changes... 13 | ## 'fix' is for bug fixes 14 | ## 'new' is for new features, big improvement 15 | ## 16 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' 17 | ## 18 | ## Is WHO is concerned by the change. 19 | ## 20 | ## 'dev' is for developpers (API changes, refactors...) 21 | ## 'usr' is for final users (UI changes) 22 | ## 'pkg' is for packagers (packaging changes) 23 | ## 'test' is for testers (test only related changes) 24 | ## 'doc' is for doc guys (doc only changes) 25 | ## 26 | ## COMMIT_MSG is ... well ... the commit message itself. 27 | ## 28 | ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' 29 | ## 30 | ## They are preceded with a '!' or a '@' (prefer the former, as the 31 | ## latter is wrongly interpreted in github.) Commonly used tags are: 32 | ## 33 | ## 'refactor' is obviously for refactoring code only 34 | ## 'minor' is for a very meaningless change (a typo, adding a comment) 35 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 36 | ## 'wip' is for partial functionality but complete subfunctionality. 37 | ## 38 | ## Example: 39 | ## 40 | ## new: usr: support of bazaar implemented 41 | ## chg: re-indentend some lines !cosmetic 42 | ## new: dev: updated code to be compatible with last version of killer lib. 43 | ## fix: pkg: updated year of licence coverage. 44 | ## new: test: added a bunch of test around user usability of feature X. 45 | ## fix: typo in spelling my name in comment. !minor 46 | ## 47 | ## Please note that multi-line commit message are supported, and only the 48 | ## first line will be considered as the "summary" of the commit message. So 49 | ## tags, and other rules only applies to the summary. The body of the commit 50 | ## message will be displayed in the changelog without reformatting. 51 | 52 | 53 | ## 54 | ## ``ignore_regexps`` is a line of regexps 55 | ## 56 | ## Any commit having its full commit message matching any regexp listed here 57 | ## will be ignored and won't be reported in the changelog. 58 | ## 59 | ignore_regexps = [ 60 | r'@minor', r'!minor', 61 | r'@cosmetic', r'!cosmetic', 62 | r'@refactor', r'!refactor', 63 | r'@wip', r'!wip', 64 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', 65 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', 66 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', 67 | ] 68 | 69 | 70 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a 71 | ## list of regexp 72 | ## 73 | ## Commit messages will be classified in sections thanks to this. Section 74 | ## titles are the label, and a commit is classified under this section if any 75 | ## of the regexps associated is matching. 76 | ## 77 | section_regexps = [ 78 | ('Feature', [ 79 | r'^[fF]eature\s*:\s*([^\n]*)$', 80 | ]), 81 | ('Changes', [ 82 | r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 83 | ]), 84 | ('Fix', [ 85 | r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 86 | ]), 87 | ('Documentation', [ 88 | r'^[dD]ocumentation\s*:\s*([^\n]*)$', 89 | ]), 90 | 91 | ('Other', None ## Match all lines 92 | ), 93 | 94 | ] 95 | 96 | 97 | ## ``body_process`` is a callable 98 | ## 99 | ## This callable will be given the original body and result will 100 | ## be used in the changelog. 101 | ## 102 | ## Available constructs are: 103 | ## 104 | ## - any python callable that take one txt argument and return txt argument. 105 | ## 106 | ## - ReSub(pattern, replacement): will apply regexp substitution. 107 | ## 108 | ## - Indent(chars=" "): will indent the text with the prefix 109 | ## Please remember that template engines gets also to modify the text and 110 | ## will usually indent themselves the text if needed. 111 | ## 112 | ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns 113 | ## 114 | ## - noop: do nothing 115 | ## 116 | ## - ucfirst: ensure the first letter is uppercase. 117 | ## (usually used in the ``subject_process`` pipeline) 118 | ## 119 | ## - final_dot: ensure text finishes with a dot 120 | ## (usually used in the ``subject_process`` pipeline) 121 | ## 122 | ## - strip: remove any spaces before or after the content of the string 123 | ## 124 | ## Additionally, you can `pipe` the provided filters, for instance: 125 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") 126 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') 127 | #body_process = noop 128 | body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip 129 | 130 | 131 | ## ``subject_process`` is a callable 132 | ## 133 | ## This callable will be given the original subject and result will 134 | ## be used in the changelog. 135 | ## 136 | ## Available constructs are those listed in ``body_process`` doc. 137 | subject_process = (strip | 138 | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | 139 | ucfirst | final_dot) 140 | 141 | 142 | ## ``tag_filter_regexp`` is a regexp 143 | ## 144 | ## Tags that will be used for the changelog must match this regexp. 145 | ## 146 | tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$' 147 | 148 | 149 | ## ``unreleased_version_label`` is a string 150 | ## 151 | ## This label will be used as the changelog Title of the last set of changes 152 | ## between last valid tag and HEAD if any. 153 | unreleased_version_label = "%%version%% (unreleased)" 154 | 155 | 156 | ## ``output_engine`` is a callable 157 | ## 158 | ## This will change the output format of the generated changelog file 159 | ## 160 | ## Available choices are: 161 | ## 162 | ## - rest_py 163 | ## 164 | ## Legacy pure python engine, outputs ReSTructured text. 165 | ## This is the default. 166 | ## 167 | ## - mustache() 168 | ## 169 | ## Template name could be any of the available templates in 170 | ## ``templates/mustache/*.tpl``. 171 | ## Requires python package ``pystache``. 172 | ## Examples: 173 | ## - mustache("markdown") 174 | ## - mustache("restructuredtext") 175 | ## 176 | ## - makotemplate() 177 | ## 178 | ## Template name could be any of the available templates in 179 | ## ``templates/mako/*.tpl``. 180 | ## Requires python package ``mako``. 181 | ## Examples: 182 | ## - makotemplate("restructuredtext") 183 | ## 184 | 185 | output_engine = rest_py 186 | #output_engine = mustache("markdown") 187 | #output_engine = makotemplate("restructuredtext") 188 | 189 | 190 | ## ``include_merge`` is a boolean 191 | ## 192 | ## This option tells git-log whether to include merge commits in the log. 193 | ## The default is to include them. 194 | include_merge = True 195 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | /rvo.egg-info/ 4 | /dist/ 5 | /build/ 6 | .cache/* 7 | .coverage 8 | /.virtualenv/ 9 | .vscode/ 10 | tags -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | # command to install dependencies 6 | install: 7 | - pip install . 8 | - python -c 'import nltk; nltk.download("book")' 9 | 10 | # command to run tests 11 | script: PYTHONPATH=. py.test --cov=rvo 12 | 13 | # Codecov.io integration 14 | before_install: 15 | - pip install codecov 16 | - pip install pytest 17 | - pip install mongomock 18 | - pip install coverage 19 | - pip install pytest-cov 20 | 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) 23 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | "rvo" is written and maintained by Florian Baumann 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Get Started! 13 | ------------ 14 | 15 | Ready to contribute? Here's how to set up `rvo` for local development. 16 | 17 | 1. Fork the `rvo` repo on GitHub. 18 | 2. Clone your fork locally:: 19 | 20 | $ git clone git@github.com:your_name_here/rvo.git 21 | 22 | 3. Install your local copy into a virtualenv. Assuming you have virtualenv installed, this is how you set up your fork for local development:: 23 | 24 | $ cd rvo/ 25 | $ mkvirtualenv rvo 26 | $ workon rvo 27 | $ python setup.py develop 28 | 29 | 4. Create a branch for local development:: 30 | 31 | $ git checkout -b name-of-your-bugfix-or-feature 32 | 33 | Now you can make your changes locally.:: 34 | 35 | $ git add . 36 | $ git commit -m "Your detailed description of your changes." 37 | $ git push origin name-of-your-bugfix-or-feature 38 | 39 | 5. Run the test suite 40 | 41 | Run the built-in unittest module in the rvo root directory:: 42 | 43 | $ cd rvo/ 44 | $ PYTHONPATH=. py.test --cov=rvo 45 | 46 | 6. Submit a pull request through the GitHub website. 47 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 5 | 23.0.6 (2017-02-25) 6 | ------------------- 7 | - Release: 23.0.6. [Florian Baumann] 8 | - Fix for emojis. [Florian Baumann] 9 | - Changelog: 23.0.5. [Florian Baumann] 10 | 11 | 12 | 23.0.5 (2017-02-25) 13 | ------------------- 14 | - Release: 23.0.5. [Florian Baumann] 15 | - Changelog: 23.0.4. [Florian Baumann] 16 | 17 | 18 | 23.0.4 (2017-02-25) 19 | ------------------- 20 | - Release: 23.0.4. [Florian Baumann] 21 | - Merge branch 'master' of github.com:noqqe/rvo. [Florian Baumann] 22 | - Changelog: 23.0.3. [Florian Baumann] 23 | - Changes. [Florian Baumann] 24 | 25 | 26 | 23.0.3 (2017-02-16) 27 | ------------------- 28 | - Release: 23.0.3. [Florian Baumann] 29 | - Release tag fix. [Florian Baumann] 30 | - Changelog: 23.0.2. [Florian Baumann] 31 | - Release: 23.0.2. [Florian Baumann] 32 | - Changelogfix. [Florian Baumann] 33 | - Release: 23.0.1. [Florian Baumann] 34 | - Release. [Florian Baumann] 35 | - Changelog: 23.0.0. [Florian Baumann] 36 | 37 | 38 | 23.0.0 (2017-02-13) 39 | ------------------- 40 | 41 | Feature 42 | ~~~~~~~ 43 | - Feature: remove emojis from titles. [Florian Baumann] 44 | 45 | Other 46 | ~~~~~ 47 | - Release: 23.0.0. [Florian Baumann] 48 | - Location. [Florian Baumann] 49 | - Fixes #8 -e flag is confusing. [Florian Baumann] 50 | - Fix list test. [Florian Baumann] 51 | - Revert "field projection for mongodb queries" [Florian Baumann] 52 | 53 | This reverts commit 2646384e7be62a128a31d1fd0e2e92b388d998e6. 54 | - 2.7 notice in readme. [Florian Baumann] 55 | - Config file for readme. [Florian Baumann] 56 | - Field projection for mongodb queries. [Florian Baumann] 57 | - Fix small error while parsing date from list command. [Florian 58 | Baumann] 59 | - Revert "Order changed" [Florian Baumann] 60 | 61 | This reverts commit 6546f8bbb566fa8d250fbdbac5bd44bc54b74d01. 62 | - Order changed. [Florian Baumann] 63 | - Ignore vscode. [Florian Baumann] 64 | - Catch error for initialized nltk data to fix #7. [Florian Baumann] 65 | - Fix more as a pager #5. [Florian Baumann] 66 | - Configurable pager options to fix #4. [Florian Baumann] 67 | - More memories test. [Florian Baumann] 68 | - Memories are testable now. [Florian Baumann] 69 | - Memories now testable. [Florian Baumann] 70 | - Download nltk stuff in travis tests. [Florian Baumann] 71 | - Codeclimate. [Florian Baumann] 72 | - Fix duplicate keys without ctx. [Florian Baumann] 73 | - History. [Florian Baumann] 74 | 75 | 76 | 22.0.0 (2016-06-30) 77 | ------------------- 78 | - Version bump. [Florian Baumann] 79 | - Change to context based command execution. Much more efficient. 80 | [Florian Baumann] 81 | - Context for config sharing. [Florian Baumann] 82 | - Fix for show to stdout. [Florian Baumann] 83 | - Fix mail. [Florian Baumann] 84 | - Foo. [Florian Baumann] 85 | - Foo. [Florian Baumann] 86 | - First version of mail import. [Florian Baumann] 87 | - Test for mailimport. [Florian Baumann] 88 | 89 | 90 | 21.0.0 (2016-05-30) 91 | ------------------- 92 | - Version bump. [Florian Baumann] 93 | - Fix for edit. [Florian Baumann] 94 | - Delete pytest. [Florian Baumann] 95 | - Tests for stats. [Florian Baumann] 96 | - Tests for stats. [Florian Baumann] 97 | - Title change. [Florian Baumann] 98 | - Rename and speedup for analyze. [Florian Baumann] 99 | - Rename and speedup for analyze. [Florian Baumann] 100 | - Fixed encoding foo. [Florian Baumann] 101 | - Code deduplication during getting of content. [Florian Baumann] 102 | - Fix tests for not implemented import. [Florian Baumann] 103 | - Memories view - a little more structured. [Florian Baumann] 104 | - Lol - authors. [Florian Baumann] 105 | - Fix readme. [Florian Baumann] 106 | - Images in readme. [Florian Baumann] 107 | - First steps in import. [Florian Baumann] 108 | - Fix export. [Florian Baumann] 109 | - Export with same normalizing as list. [Florian Baumann] 110 | - Regex now on tags and categories. [Florian Baumann] 111 | - List output sorting now configurable. [Florian Baumann] 112 | - Fix for reverse sorting in text analysis. [Florian Baumann] 113 | - Badges in readme. [Florian Baumann] 114 | - Test. [Florian Baumann] 115 | - Travis. [Florian Baumann] 116 | - Codecoverage. [Florian Baumann] 117 | - Travisfix. [Florian Baumann] 118 | - Travis fix. [Florian Baumann] 119 | - Travis fix. [Florian Baumann] 120 | - Travis fix. [Florian Baumann] 121 | - Travis. [Florian Baumann] 122 | - Readme fix. [Florian Baumann] 123 | 124 | 125 | 20.0.0 (2016-05-01) 126 | ------------------- 127 | - Init. [Florian Baumann] 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Florian Baumann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/noqqe/rvo.svg?branch=master 2 | :target: https://travis-ci.org/noqqe/rvo 3 | 4 | .. image:: https://codecov.io/gh/noqqe/rvo/branch/master/graph/badge.svg 5 | :target: https://codecov.io/gh/noqqe/rvo 6 | 7 | .. image:: https://codeclimate.com/github/noqqe/rvo/badges/gpa.svg 8 | :target: https://codeclimate.com/github/noqqe/rvo 9 | :alt: Code Climate 10 | 11 | .. image:: https://raw.github.com/noqqe/rvo/master/images/rvo.png 12 | 13 | rvo 14 | === 15 | 16 | I use ``rvo`` for managing my text data. 17 | 18 | 19 | - Notes 20 | - Docs (personal wiki) 21 | - Bookmarks 22 | - Journal 23 | - Quotes 24 | 25 | and the like. 26 | 27 | Motivations 28 | ~~~~~~~~~~~ 29 | 30 | When I started writing rvo, my goal was designing an utility that feels 31 | a little like markdown. It should be usable very easily. A handy program 32 | the user likes to use. At the same time it should be easy to 33 | integrate with other tools. Taking the typical unix approach. Make it 34 | read and write to stdin/out. Have it fully configurable with commandline 35 | parameters. 36 | 37 | It basically should not matter if a human or a machine is interacting 38 | with ``rvo``. 39 | 40 | Features 41 | ~~~~~~~~ 42 | 43 | .. image:: https://raw.github.com/noqqe/rvo/master/images/screenshot.png 44 | 45 | - Rich search capabilities 46 | - Statistics about the documents 47 | - Encryption of single documents using Salsa20 and Blake2b 48 | - Interacts with ``vim`` 49 | - Fetching the title of a link to be the title of your document 50 | - Mail an article to a friend of yours. 51 | - Easily interact with other programs using stdin / stdout 52 | 53 | Installation 54 | ~~~~~~~~~~~~ 55 | 56 | Currently, rvo only works in python2.7 57 | 58 | :: 59 | 60 | pip install rvo 61 | 62 | After that, setup a mongodb. You may use your favorite packagemanager. 63 | For me it's OpenBSDs "pkg_add". 64 | 65 | :: 66 | 67 | /etc/init.d/mongod start 68 | 69 | Configuration 70 | ~~~~~~~~~~~~~ 71 | 72 | Add this to the config file ``~/.rvo.conf`` 73 | 74 | :: 75 | 76 | [General] 77 | MongoDB = mongodb://localhost/ 78 | DB = rvo 79 | Editor = vim 80 | Pager = vim 81 | PagerOptions = -R 82 | MailFrom = user@example.net 83 | 84 | After it was successfully configured, run ping to verfiy 85 | the connection to mongodb works. 86 | 87 | :: 88 | 89 | $ rvo ping 90 | >>> Trying to connect to database... 91 | >>> Connection SUCCESSFUL 92 | >>> Trying to write, read, delete to test_collection... 93 | >>> Interactions were SUCCESSFUL 94 | 95 | Quickstart 96 | ~~~~~~~~~~ 97 | 98 | `rvo` is a really friendly piece of software. It helps you whereever it needs to. 99 | You may start with a simple 100 | 101 | :: 102 | 103 | $ rvo --help 104 | 105 | To add a document, just 106 | 107 | :: 108 | 109 | $ rvo add 110 | 111 | and tada, your very first document is created. Add content from whatever you like. As said before, 112 | you can store notes, write diary. After that, check out your documents. 113 | 114 | :: 115 | 116 | $ rvo list 117 | 118 | 119 | Thats all. Just kidding. Have a look into all the the other commands! 120 | 121 | :: 122 | 123 | add Add a document. 124 | analyze Text analysis on a document 125 | append Append content to a document. 126 | delete Deletes a document. 127 | edit Edit the content of an document. 128 | export Exports all document 129 | info Shows metadata of a document 130 | list List documents from store 131 | log Show transactions 132 | mail Mails a document to a recipient for your choice 133 | memories Some nostalgia by showing documents some years ago 134 | modify Modifies a documents metadata 135 | ping Verifys the connection to the database 136 | show Shows a document 137 | stats Show statistics about categories and tags 138 | 139 | Document titles 140 | ~~~~~~~~~~~~~~~ 141 | 142 | The title from a normal document is generated from the first line of the 143 | content. Leading whitespace and ``#`` will be stripped away. 144 | 145 | :: 146 | 147 | $ rvo add -t javascript -c notes 148 | # Meeting Notes 149 | 150 | * We should probably throw away our .js applications 151 | * ... 152 | 153 | So "Meeting Notes" will be the document title. This also happens when you edit 154 | a document. So if you want to change the title, edit the content and after 155 | saving the title gets updated. 156 | 157 | Stdout 158 | ~~~~~~ 159 | 160 | Normally, ``rvo`` opens your favorite ``PAGER``. If output redirection 161 | is detected it just outputs plain content to whatever file you like. 162 | 163 | :: 164 | 165 | $ rvo list -c meeting 166 | $ rvo show 1 > /tmp/meeting.md 167 | 168 | Also without redirection the content is being ``cat`` ed by using the ``-s`` flag 169 | 170 | :: 171 | 172 | $ rvo show -s 2 173 | 174 | Stdin 175 | ~~~~~ 176 | 177 | Read content from stdin 178 | 179 | :: 180 | 181 | $ echo foo | rvo add -t test -c notes 182 | 183 | Export 184 | ~~~~~~ 185 | 186 | You can easily export all what you've inserted. 187 | 188 | :: 189 | 190 | rvo export -c twitter --to json | python -m json.tool 191 | rvo export -t work --to markdown 192 | 193 | Or just loop over the output 194 | 195 | :: 196 | 197 | rvo list -l 5000 198 | for x in {1..5000} ; do rvo show --stdout $x ; done 199 | 200 | Document identification 201 | ~~~~~~~~~~~~~~~~~~~~~~~ 202 | 203 | As a typical workflow, you do a list query and You can either use the 204 | full mongodb objectid or a shortid. 205 | 206 | Everytime you do a list query, a resultset will be built. Every result 207 | gets a shortid assigned to it and this mapping is being saved in 208 | mongodb. 209 | 210 | I've implemented shortids because they are easier to use. You dont have 211 | to copy the full objectid using copy with mouse. ``shortids`` are easier 212 | to use! 213 | 214 | Crypto 215 | ~~~~~~ 216 | 217 | The crypto used is written with `Salsa20` and `blake2b`. When the first 218 | document is created and being encrypted, rvo prompts for the initial password. 219 | Keep this password save. You will need it more often. 220 | 221 | The password you set is used to encrypt a randomly generated character long 222 | password. Its stored within the database. Most important. The generated password 223 | is used to encrypt and decrypt every document (when encryption is set). 224 | 225 | Basically that means: there is one password (chosen by you) that unlocks 226 | another generated password, that encrypts your document. 227 | 228 | This ensures a lot of stuff. For example easy password changes for the user. 229 | Or setting a slightly different password accidentially for one document. 230 | 231 | 232 | Links 233 | ~~~~~ 234 | 235 | Links: If the content is just an url, it gets automatically the category 236 | ``links`` and its html title will be fetched to be used as ``title`` 237 | within the document. 238 | 239 | Development 240 | ----------- 241 | 242 | Wording. 243 | 244 | - docid is what is being used to identify a document. It can be both, a 245 | shortid or a ObjectId (MongoDB) 246 | 247 | - Documentstore basically means mongodb at the moment 248 | 249 | - All commands have to be stored in submodule commands and can contain 250 | only 1 command that has to be named exactly as the filename is. This 251 | is required for click to parse all commands. 252 | 253 | Data Structure 254 | ~~~~~~~~~~~~~~ 255 | 256 | The native json document that goes into MongoDB looks like this 257 | 258 | :: 259 | 260 | { 261 | "_id" : ObjectId("568d344c6815b45596d1c7ad"), 262 | "title": "My very first entry" 263 | "content" : "", 264 | "created": ISODate("2014-09-03T07:37:52Z"), 265 | "updated": ISODate("2015-09-03T07:37:52Z"), 266 | "tags": [ "mongodb", "markdown" ], 267 | "category": ["notes"], 268 | "encrypted": false, 269 | } 270 | 271 | Since rvo uses ``pymongo``, its way easier dealing with documents. 272 | Python native types are automatically converted to the corresponding 273 | types in json/mongodb. The following is a native python dictionary. 274 | 275 | :: 276 | 277 | { 278 | 'title': '2-Factor-Auth', 279 | 'content': '', 280 | 'created': datetime.datetime(), 281 | 'updated': datetime.datetime(), 282 | 'tags': ['markdown, 'mongodb'], 283 | 'encrypted': False, 284 | 'categories': ['notes'], 285 | } 286 | 287 | Missing 288 | ~~~~~~~ 289 | 290 | There are also features, that rvo does not have and probably never gets. 291 | 292 | - Version control for your documents 293 | - Multiple users or an "author" field. 294 | 295 | Last but not least 296 | ~~~~~~~~~~~~~~~~~~ 297 | 298 | Do not confuse `rvo` with http://www.rvo.nl. Rijksdienst voor Ondernemend Nederland. 299 | It has nothing to do with it. Still, I really like their logo. 300 | -------------------------------------------------------------------------------- /images/rvo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noqqe/rvo/423e1ea1aea0a2dc849ceae838e18896a13e7771/images/rvo.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noqqe/rvo/423e1ea1aea0a2dc849ceae838e18896a13e7771/images/screenshot.png -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # conf 5 | VERFILE=setup.py 6 | 7 | if [[ $1 != "major" ]] && [[ $1 != "minor" ]] && [[ $1 != "patch" ]]; then 8 | echo wrong usage. use major/minor/patch as first argument 9 | exit 1 10 | fi 11 | 12 | function get_cur_vers { 13 | grep '^version = "' $VERFILE | awk -F\" '{print $2}' 14 | } 15 | 16 | echo bump version 17 | bumpversion --current-version $(get_cur_vers) $1 $VERFILE 18 | 19 | v=$(get_cur_vers) 20 | 21 | echo adding local file 22 | git add $VERFILE 23 | 24 | echo commit 25 | git commit -m "Release: $v" 26 | 27 | echo tagging.. 28 | git tag ${v} 29 | 30 | echo creating history.rst 31 | gitchangelog > HISTORY.rst 32 | git add HISTORY.rst 33 | 34 | echo committing history 35 | git commit -m "Changelog: $v" 36 | 37 | echo pushing.. 38 | git push --tags origin master 39 | 40 | echo release on pypi 41 | python setup.py sdist upload -r pypi 42 | 43 | -------------------------------------------------------------------------------- /rvo/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | from pkg_resources import get_distribution 3 | __version__ = get_distribution('rvo').version 4 | -------------------------------------------------------------------------------- /rvo/analysis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | import sys 6 | import nltk 7 | import datetime 8 | import hurry.filesize 9 | 10 | def get_sentences(content): 11 | """ 12 | Returns the number of sentences 13 | :content: str 14 | :returns: int 15 | """ 16 | sentences = len(nltk.tokenize.sent_tokenize(content)) 17 | return sentences 18 | 19 | def get_words(content): 20 | """ 21 | Returns the number of sentences 22 | :content: str 23 | :returns: int 24 | """ 25 | words = len(nltk.word_tokenize(content)) 26 | return words 27 | 28 | def get_characters(content): 29 | """ 30 | Returns the number of sentences 31 | :content: str 32 | :returns: int 33 | """ 34 | characters = len(content) 35 | return characters 36 | 37 | def get_word_distribution(content): 38 | """ 39 | Returns a distribution file on words 40 | """ 41 | 42 | # remove urls 43 | content = re.sub(r'^https?:\/\/.*[\r\n]*', '', content, flags=re.MULTILINE) 44 | 45 | # Remove non alphanumeric chars 46 | tokenizer = nltk.tokenize.RegexpTokenizer(r'\w+') 47 | words = tokenizer.tokenize(content) 48 | 49 | # Remove single-character tokens (mostly punctuation) 50 | words = [word for word in words if len(word) > 1] 51 | 52 | # Remove numbers 53 | words = [word for word in words if not word.isnumeric()] 54 | 55 | # Lowercase all words (default_stopwords are lowercase too) 56 | words = [word.lower() for word in words] 57 | 58 | # Remove numbers 59 | words = [word for word in words if not word.isnumeric()] 60 | 61 | # Remove english stopwords 62 | words = [word for word in words if word not in nltk.corpus.stopwords.words('english')] 63 | 64 | # Remove german stopwords 65 | words = [word for word in words if word not in nltk.corpus.stopwords.words('german')] 66 | 67 | 68 | # Calculate frequency distribution 69 | fdist = nltk.FreqDist(words) 70 | 71 | return fdist 72 | 73 | 74 | def get_most_common_words(fdist, count): 75 | """ 76 | Get list of words that are most commonly 77 | used in this text 78 | :fdist: nltk object 79 | :words: int - number of words as result 80 | :returns: tuple - (words,frequency) 81 | """ 82 | return fdist.most_common(count) 83 | 84 | def get_least_common_words(fdist, count): 85 | """ 86 | Get list of words that are least commonly 87 | used in this text 88 | :fdist: nltk object 89 | :count: int - number of words as result 90 | :returns: list 91 | """ 92 | return fdist.hapaxes()[0:count] 93 | 94 | def get_words_per_sentence(content): 95 | """ 96 | Get words per sentance average 97 | :content: str 98 | :returns: int 99 | """ 100 | words = get_words(content) 101 | sentences = get_sentences(content) 102 | return words / sentences 103 | 104 | def get_long_words(fdist,count): 105 | """ 106 | Get list of words that are least commonly 107 | used in this text 108 | :content: str 109 | :count: int - number of words as result 110 | :returns: list 111 | """ 112 | longwords = [] 113 | for word in fdist: 114 | if len(word) > 10 and len(word) < 70: 115 | longwords.append(word) 116 | 117 | longwords = sorted(longwords, key=len, reverse=True) 118 | 119 | return longwords[0:count] 120 | 121 | def get_age_of_document(created): 122 | """ 123 | Returns the age of a document as 124 | string. 125 | :created: date 126 | :returns: str 127 | """ 128 | today = datetime.datetime.now() 129 | age = today - created 130 | return str(age) 131 | 132 | def get_size_of_document(content): 133 | """ 134 | Returns human readable size of document 135 | :content: str 136 | :returns: str 137 | """ 138 | s = sys.getsizeof(content) 139 | s = hurry.filesize.size(s, system=hurry.filesize.alternative) 140 | 141 | return s 142 | -------------------------------------------------------------------------------- /rvo/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | import sys 4 | import pymongo 5 | import os 6 | import click 7 | import datetime 8 | import rvo.utils as utils 9 | from rvo import __version__ 10 | import rvo.config 11 | 12 | command_folder = os.path.join(os.path.dirname(__file__), 'commands') 13 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 14 | 15 | # rvo command class 16 | class rvoCommands(click.MultiCommand): 17 | 18 | def list_commands(self, ctx): 19 | rv = [] 20 | for filename in os.listdir(command_folder): 21 | #if filename.endswith('.py'): 22 | if filename.endswith('.py') and not filename.startswith('__init__'): 23 | rv.append(filename[:-3]) 24 | rv.sort() 25 | return rv 26 | 27 | def get_command(self, ctx, name): 28 | ns = {} 29 | fn = os.path.join(command_folder, name + '.py') 30 | try: 31 | with open(fn) as f: 32 | code = compile(f.read(), fn, 'exec') 33 | eval(code, ns, ns) 34 | return ns[name] 35 | except IOError: 36 | click.help_option() 37 | 38 | # base help message 39 | @click.command(cls=rvoCommands, context_settings=CONTEXT_SETTINGS, 40 | help=""" 41 | Manage text data on commandline 42 | 43 | \b 44 | 888,8, Y8b Y888P e88 88e 45 | 888 " Y8b Y8P d888 888b 46 | 888 Y8b " Y888 888P 47 | 888 Y8P "88 88" 48 | 49 | For the sake of your own data being managed 50 | by you and only you! 51 | 52 | """) 53 | @click.version_option(version=__version__, prog_name="rvo") 54 | @click.pass_context 55 | def cli(ctx): 56 | ctx.obj = {} 57 | ctx.obj['config'] = rvo.config.parse_config() 58 | ctx.obj['db'] = pymongo.MongoClient(ctx.obj["config"]["uri"]) 59 | 60 | if __name__ == '__main__': 61 | cli() 62 | 63 | -------------------------------------------------------------------------------- /rvo/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noqqe/rvo/423e1ea1aea0a2dc849ceae838e18896a13e7771/rvo/commands/__init__.py -------------------------------------------------------------------------------- /rvo/commands/add.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import click 4 | import fileinput 5 | import datetime 6 | import rvo.config 7 | import rvo.db as db 8 | import rvo.views as views 9 | import rvo.utils as utils 10 | import rvo.transaction as transaction 11 | from rvo.validate import validate, validate_date, validate_location 12 | from rvo.crypto import crypto 13 | 14 | @click.command(help=""" 15 | Add a document. 'add' is made to be very intuitive and functional. 16 | You can add and set all fields of a document during creation. 17 | 18 | It also supports reading from stdin. Unless this isn't the case 19 | it will start up your configured editor. 20 | """) 21 | @click.option('encrypt', '-e' ,'--encrypted', default=False, is_flag=True, help='Encrypt this document') 22 | @click.option('tags', '-t' ,'--tag', 23 | type=str, required=False, show_default=False, multiple=True, 24 | help='Set tags for this document') 25 | @click.option('categories', '-c' ,'--category', 26 | type=str, required=False, show_default=False, multiple=True, 27 | help='Set categories for this document') 28 | @click.option('date', '-d', '--date', default=datetime.datetime.now(), 29 | help='Set a custom creation date for this document', callback=validate_date) 30 | @click.option('location', '-l', '--location', default=None, required=False, 31 | help='Set a location for this document', callback=validate_location) 32 | @click.option('password', '-p', '--password', required=False, default=False, 33 | help="Password for encrypted documents") 34 | @click.option('content', '-x', '--content', default=False, required=False, help='Read content from parameter') 35 | @click.pass_context 36 | def add(ctx, date, tags, categories, location, content, password, encrypt): 37 | """ 38 | Adds a new document with various meta data options. 39 | """ 40 | 41 | config = ctx.obj["config"] 42 | 43 | # Kindly ask for password when encryption comes 44 | if encrypt: 45 | c = crypto(ctx, password) 46 | if not c: 47 | sys.exit(1) 48 | 49 | # If input from -x parameter, encode in utf8 50 | try: 51 | content = content.encode("utf8") 52 | except (AttributeError, UnicodeEncodeError): 53 | pass 54 | 55 | # Read content directly from stdin if: 56 | # * there is no content from -x 57 | # * document should not be encrypted (conflicts with password prompt) 58 | # * there is piped input 59 | if not content and not encrypt and not utils.isatty: 60 | content = "" 61 | for l in click.get_text_stream('stdin'): 62 | content = content + l 63 | if content == "": 64 | content = False 65 | 66 | # If there is still no content and there is an interactive terminal, 67 | # open the editor to get query 68 | if not content and utils.isatty: 69 | content, tmpfile = utils.get_content_from_editor(config["editor"]) 70 | 71 | # If everything fails, fail silently and let the user know. 72 | if not content: 73 | utils.log_error("Could not get any content") 74 | sys.exit(1) 75 | 76 | # Generate title from content 77 | title = utils.get_title_from_content(content) 78 | 79 | # Remove emojis 80 | title = utils.remove_emojis(title) 81 | 82 | # Encrypt 83 | if encrypt is True: 84 | content = c.encrypt_content(content) 85 | 86 | # if content looks like a url, set category and fetch title 87 | if re.compile("^https?://").search(content.lower()) is not None: 88 | 89 | # remove whitespace from right side 90 | content = content.rstrip() 91 | 92 | # check for duplicates 93 | if db.check_for_duplicate(ctx, field="url", content=content) is True: 94 | utils.log_info("Duplicate found") 95 | 96 | # fetch title and set category 97 | title = utils.get_title_from_webpage(content) 98 | 99 | categories = ["links"] 100 | 101 | # location 102 | loc = {} 103 | if location is None: 104 | loc["address"] = None 105 | loc["lat"] = None 106 | loc["long"] = None 107 | else: 108 | loc["address"] = location.address 109 | loc["lat"] = location.latitude 110 | loc["long"] = location.longitude 111 | 112 | # build item to insert into collection 113 | item = { 114 | "title": title, 115 | "content": content, 116 | "tags": list(tags), 117 | "categories": list(categories), 118 | "created": date, 119 | "updated": date, 120 | "encrypted": encrypt, 121 | "location": loc, 122 | } 123 | 124 | # insert item if its valid 125 | if validate(item): 126 | coll = db.get_document_collection(ctx) 127 | docid = coll.insert_one(item).inserted_id 128 | 129 | transaction.log(ctx, str(docid), "add", title) 130 | utils.log_info("Document \"%s\" created." % title) 131 | 132 | else: 133 | utils.log_error("Validation of the updated object did not succeed") 134 | 135 | try: 136 | utils.clean_tmpfile(tmpfile) 137 | except UnboundLocalError: 138 | pass 139 | 140 | return True 141 | -------------------------------------------------------------------------------- /rvo/commands/append.py: -------------------------------------------------------------------------------- 1 | import click 2 | import datetime 3 | from bson import ObjectId 4 | import rvo.db as db 5 | import rvo.utils as utils 6 | import rvo.transaction as transaction 7 | from rvo.crypto import crypto 8 | from rvo.validate import validate 9 | 10 | @click.command(help=""" 11 | Append content to a document. 12 | This is useful for programmatically adding content. 13 | """) 14 | @click.option('password', '-p', '--password', required=False, default=False, 15 | help="Password for encrypted documents") 16 | @click.option('content', '-x', '--content', required=False, default=False, 17 | help="Content to be appended to the document") 18 | @click.argument('docid') 19 | @click.pass_context 20 | def append(ctx, docid, password, content): 21 | """ 22 | Append string to a document 23 | 24 | Trys to find the object, (decrypt,) adds content to the bottom, 25 | (encrypt,) update date fields and regenerates the title. 26 | 27 | :docid: str (bson object) 28 | :returns: bool 29 | """ 30 | 31 | coll = db.get_document_collection(ctx) 32 | doc, docid = db.get_document_by_id(ctx, docid) 33 | 34 | template, c = db.get_content(ctx, doc, password=password) 35 | 36 | d = datetime.datetime.now() 37 | 38 | if not content: 39 | content = "" 40 | for l in click.get_text_stream('stdin'): 41 | content = content + l 42 | 43 | content = template + content 44 | 45 | if isinstance(content, unicode): 46 | content = content.encode("utf-8") 47 | 48 | if doc["encrypted"] is True: 49 | title = utils.get_title_from_content(content) 50 | content = c.encrypt_content(content) 51 | else: 52 | if not "links" in doc["categories"]: 53 | title = utils.get_title_from_content(content) 54 | 55 | 56 | if content != template: 57 | doc["content"] = content 58 | doc["title"] = title 59 | doc["updated"] = d 60 | if validate(doc): 61 | coll.save(doc) 62 | transaction.log(ctx, docid, "append", title) 63 | utils.log_info("Content appended to \"%s\"." % title) 64 | else: 65 | utils.log_error("Validation of the updated object did not succeed") 66 | else: 67 | utils.log_info("No changes detected for \"%s\"" % title) 68 | 69 | return True 70 | -------------------------------------------------------------------------------- /rvo/commands/delete.py: -------------------------------------------------------------------------------- 1 | import click 2 | from bson import ObjectId 3 | import rvo.db as db 4 | import rvo.views as views 5 | import rvo.utils as utils 6 | import rvo.transaction as transaction 7 | 8 | @click.command(help=""" 9 | Deletes a document. 10 | 11 | Be careful. Delete means, its gone. Gone gone. 12 | There is no trashbin whatever. Its like burning a 13 | piece of paper. Maybe I should configure an alias to 'burn'. 14 | """) 15 | @click.argument('docid') 16 | @click.option('yes', '-y' ,'--yes', default=False, is_flag=True, help='Dont ask for confirmation') 17 | @click.pass_context 18 | def delete(ctx, docid, yes): 19 | """ 20 | Deletes a document from the documentstore 21 | :docid: str (will be converted in bson object) 22 | :returns: bool 23 | """ 24 | doc, docid = db.get_document_by_id(ctx, docid) 25 | views.detail(doc) 26 | coll = db.get_document_collection(ctx) 27 | 28 | t = doc["title"] 29 | if yes: 30 | coll.remove({"_id": ObjectId(docid)}) 31 | utils.log_info("Removed %s" % t) 32 | else: 33 | if click.confirm("%s Are you sure, you want to delete this?" % utils.query_prefix): 34 | coll.remove({"_id": ObjectId(docid)}) 35 | utils.log_info("Removed %s" % t) 36 | transaction.log(ctx, docid, "delete", t) 37 | return True 38 | 39 | -------------------------------------------------------------------------------- /rvo/commands/edit.py: -------------------------------------------------------------------------------- 1 | import click 2 | import datetime 3 | from bson import ObjectId 4 | import rvo.db as db 5 | import rvo.utils as utils 6 | import rvo.transaction as transaction 7 | from rvo.crypto import crypto 8 | from rvo.validate import validate 9 | import rvo.config 10 | 11 | @click.command(help=""" 12 | Edit the content of an document. 13 | 14 | Your configured editor will be opened and you 15 | are free to type any content you like. 16 | Once you save and close the editor, content will be 17 | read and added to the store. 18 | 19 | If the content does not get changed, rvo detects 20 | there was no change and does not update. 21 | """) 22 | @click.argument('docid') 23 | @click.option('password', '-p', '--password', required=False, default=False, 24 | help="Password for encrypted documents") 25 | @click.pass_context 26 | def edit(ctx, docid, password): 27 | """ 28 | Edit the content of an document. 29 | Trys to find the object, (decrypt,) read from editor, 30 | encrypt, update date fields and regenerates the title. 31 | :docid: str (bson object) 32 | :returns: bool 33 | """ 34 | coll = db.get_document_collection(ctx) 35 | config = ctx.obj["config"] 36 | 37 | doc, docid = db.get_document_by_id(ctx, docid) 38 | title = doc["title"] 39 | 40 | template, c = db.get_content(ctx, doc, password=password) 41 | 42 | content, tmpfile = utils.get_content_from_editor(config["editor"], template=template) 43 | d = datetime.datetime.now() 44 | 45 | if doc["encrypted"] is True: 46 | title = utils.get_title_from_content(content) 47 | content = c.encrypt_content(content.decode("utf-8").encode("utf-8")) 48 | else: 49 | if not "links" in doc["categories"]: 50 | title = utils.get_title_from_content(content) 51 | 52 | if isinstance(template, unicode): 53 | content = content.decode("utf-8") 54 | 55 | if content != template: 56 | doc["content"] = content 57 | doc["title"] = title 58 | doc["updated"] = d 59 | if validate(doc): 60 | coll.save(doc) 61 | else: 62 | utils.log_error("Validation of the updated object did not succeed") 63 | 64 | transaction.log(ctx, docid, "edit", title) 65 | utils.log_info("Document \"%s\" updated." % title) 66 | else: 67 | utils.log_info("No changes detected for \"%s\"" % title) 68 | 69 | utils.clean_tmpfile(tmpfile) 70 | 71 | return True 72 | -------------------------------------------------------------------------------- /rvo/commands/export.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import click 3 | import pprint 4 | from bson import ObjectId 5 | from bson.json_util import dumps 6 | import rvo.db as db 7 | import rvo.utils as utils 8 | import rvo.transaction as transaction 9 | from rvo.crypto import crypto 10 | import rvo.config 11 | 12 | @click.command(short_help="Exports all document", 13 | help=""" 14 | Exports documents. 15 | 16 | In case you want to export some of your documents 17 | to another piece of code, or build up a website out of 18 | some of the contents, you can export documents. 19 | 20 | You are free to choose the output format. 21 | You can also filter on specific documents to export on category 22 | or on tag. 23 | """) 24 | @click.option('format', '--to', required=False, default="json", 25 | type=click.Choice(['json', 'markdown']), 26 | help="Specify the format of the exported output") 27 | @click.option('password', '-p', '--password', required=False, default=False, 28 | help="Password for encrypted documents") 29 | @click.option('-c', '--category', type=str, multiple=True, 30 | help='Exports need to be in this category') 31 | @click.option('-t', '--tag', type=str, multiple=True, 32 | help='Exports have to contain this tag') 33 | @click.option('objectid', '-i', '--id', type=str, default=False, 34 | help='Exports document by id') 35 | @click.pass_context 36 | def export(ctx, format, password, category, tag, objectid): 37 | """ 38 | Shows a single object from database 39 | and opens its content in a pager 40 | :docid: string (will be converted to bson object) 41 | :returns: bool 42 | """ 43 | 44 | if objectid: 45 | docs = [] 46 | doc, docid = db.get_document_by_id(ctx, str(objectid)) 47 | docs.append(doc) 48 | else: 49 | 50 | tags = utils.normalize_element(tag, "tags") 51 | categories = utils.normalize_element(category, "categories") 52 | 53 | query = {"$and": [ tags, categories ] } 54 | 55 | coll = db.get_document_collection(ctx) 56 | 57 | config = ctx.obj["config"] 58 | 59 | docs = coll.find(query).sort("updated", -1) 60 | 61 | if format == "json": 62 | export_json(ctx, docs, password) 63 | if format == "markdown": 64 | export_markdown(ctx, docs, password) 65 | 66 | return True 67 | 68 | def export_json(ctx, docs, password): 69 | jsondata = [] 70 | for doc in docs: 71 | if doc["encrypted"] is True and password is not False: 72 | doc["content"], c = db.get_content(ctx, doc, password=password) 73 | jsondata.append(doc) 74 | 75 | print(dumps(jsondata)) 76 | 77 | def export_markdown(ctx, docs, password): 78 | for doc in docs: 79 | if doc["encrypted"] is True and password is not False: 80 | doc["content"], c = db.get_content(ctx, doc, password=password) 81 | else: 82 | doc["content"] = doc["content"].encode('utf8') 83 | 84 | print doc["content"] 85 | -------------------------------------------------------------------------------- /rvo/commands/info.py: -------------------------------------------------------------------------------- 1 | import click 2 | import rvo.db as db 3 | import rvo.transaction as transaction 4 | import rvo.views as views 5 | import rvo.analysis as analysis 6 | 7 | @click.command(short_help="Shows metadata of a document", 8 | help=""" 9 | Shows metadata of a document. 10 | 11 | Metadata matters! NSA kills because of metadata. 12 | """) 13 | @click.argument('docid') 14 | @click.pass_context 15 | def info(ctx, docid): 16 | """ 17 | Shows all available meta data belonging to a signel document 18 | :docid: string (will be converted to bson object) 19 | :returns: bool 20 | """ 21 | doc, docid = db.get_document_by_id(ctx, docid) 22 | transaction.log(ctx, docid, "info", doc["title"]) 23 | views.detail(doc) 24 | return True 25 | -------------------------------------------------------------------------------- /rvo/commands/list.py: -------------------------------------------------------------------------------- 1 | import re 2 | import click 3 | import datetime 4 | import rvo.db as db 5 | import rvo.utils as utils 6 | import rvo.views as views 7 | from rvo.validate import validate_date 8 | 9 | @click.command(short_help="List documents from store", 10 | help=""" 11 | Lists documents from the documentstore 12 | 13 | By default, the 10 latest documents will be shown. 14 | As documents grow, it gets difficult to find those. 15 | 16 | Filters can be applied on list command. 17 | See options below on how to filter the output. 18 | 19 | """) 20 | @click.option('-t', '--tag', type=str, multiple=True, 21 | help='Results have to contain this tag') 22 | @click.option('-c', '--category', type=str, multiple=True, 23 | help='Results need to be in this category') 24 | @click.option('-s', '--title', type=str, multiple=True, 25 | help='Results need to contain this string in title') 26 | @click.option('-x', '--content', type=str, multiple=True, 27 | help='Results need to contain this string in content') 28 | @click.option('-l', '--limit', type=int, default=10, 29 | help='Limit the number of results') 30 | @click.option('datefrom', '-f', '--from', default=datetime.datetime.utcfromtimestamp(0), 31 | callback=validate_date, 32 | help='Results by date starting from') 33 | @click.option('dateto', '-d', '--to', default=datetime.datetime.now(), 34 | callback=validate_date, 35 | help='Results by date ending at date') 36 | @click.option('-o', '--order', type=click.Choice(['created', 'updated']), default="updated", 37 | help='Specify sorting of the results') 38 | @click.pass_context 39 | def list(ctx, tag, category, title, content, limit, dateto, datefrom, order): 40 | """ 41 | Lists documents from database based on the filters 42 | given. First stage is triggering the filter parser, 43 | building the query and finally printing results by 44 | calling the view. 45 | :filters: string (formatted as l:foo or x:bar) 46 | :returns: bool 47 | """ 48 | coll = db.get_document_collection(ctx) 49 | 50 | tags = utils.normalize_element(tag, "tags") 51 | categories = utils.normalize_element(category, "categories") 52 | title = utils.normalize_element(title, "title") 53 | content = utils.normalize_element(content, "content") 54 | 55 | df = { "updated": { "$gte": datefrom }} 56 | de = { "updated": { "$lte": dateto }} 57 | 58 | query = {"$and": [ tags, categories, content, title, de, df ] } 59 | 60 | print("") 61 | documents = {} 62 | c = 0 63 | 64 | # Fetch results from collection and feed it into a numbered 65 | # dictonary 66 | for doc in coll.find(query).sort(order, -1).limit(limit): 67 | c += 1 68 | documents[c] = doc 69 | 70 | # If there are less entries in collection 71 | # then the limit is, overwrite limit with c 72 | if c < limit: 73 | limit = c 74 | 75 | db.clean_shortids(ctx) 76 | # reverse the order using the numbered keys 77 | # and display the object 78 | for x in reversed(range(1, limit+1)): 79 | db.map_shortid(ctx, sid=x, oid=documents[x]["_id"]) 80 | documents[x]["sid"] = x 81 | 82 | views.table(documents, limit+1) 83 | 84 | # print summary of results 85 | results = coll.find(query).count() 86 | utils.log_info("%s out of %s result(s)." % (len(documents), results)) 87 | 88 | return True 89 | 90 | -------------------------------------------------------------------------------- /rvo/commands/log.py: -------------------------------------------------------------------------------- 1 | import click 2 | import rvo.db as db 3 | import rvo.views as views 4 | 5 | @click.command(short_help="Show transactions", 6 | help=""" 7 | Transactions are logged informations 8 | about changes and access to the documents. 9 | 10 | `log' is used to get those transactions listed. 11 | 12 | Having a hard time remembering what you did? 13 | """) 14 | @click.option('entries', '-e', '--entries', default=15, type=int, 15 | help='Number of entries being shown') 16 | @click.pass_context 17 | def log(ctx, entries): 18 | """ 19 | Shows n latest transactions 20 | :n: int 21 | :returns: bool 22 | """ 23 | coll = db.get_transactions_collection(ctx) 24 | 25 | SUM = {} 26 | c = 0 27 | print("") 28 | for doc in coll.find({}).sort("date", -1).limit(entries): 29 | c += 1 30 | SUM[c] = doc 31 | 32 | views.transactions(SUM, c+1) 33 | 34 | return True 35 | 36 | -------------------------------------------------------------------------------- /rvo/commands/mail.py: -------------------------------------------------------------------------------- 1 | import click 2 | import rvo.db as db 3 | import rvo.utils as utils 4 | import rvo.transaction as transaction 5 | import rvo.config 6 | import simplemail 7 | 8 | 9 | @click.command(short_help="Mails a document to a recipient for your choice", 10 | help=""" 11 | Mails a document to a recipient for your choice. 12 | Its required to have a local unauthenticated smtpd running 13 | otherwise sending mails will fail. 14 | 15 | Im so s0rr3y. 16 | """) 17 | @click.option('to', '-t', '--to', type=str, prompt="%s To" % utils.query_prefix, 18 | help='Recipient of the mail') 19 | @click.argument('docid') 20 | @click.pass_context 21 | def mail(ctx, docid, to): 22 | """ 23 | Mails a document to a recipient for your choice 24 | Its required to have a local unauthenticated smtpd running 25 | otherwise sending mails will fail 26 | :docid: str (Bson Object) 27 | :returns: bool 28 | """ 29 | 30 | coll = db.get_document_collection(ctx) 31 | config = ctx.obj["config"] 32 | doc, docid = db.get_document_by_id(ctx, docid) 33 | 34 | try: 35 | simplemail.Email( 36 | smtp_server = "localhost", 37 | from_address = config["mailfrom"], 38 | to_address = to, 39 | subject = unicode(doc["title"]), 40 | message = doc["content"].encode("utf-8") 41 | ).send() 42 | except: 43 | utils.log_error("Error trying to send mail. May check your configuration.") 44 | -------------------------------------------------------------------------------- /rvo/commands/memories.py: -------------------------------------------------------------------------------- 1 | import re 2 | import click 3 | import datetime 4 | from dateutil.relativedelta import relativedelta 5 | import rvo.db as db 6 | import rvo.utils as utils 7 | import rvo.views as views 8 | from rvo.validate import validate_date 9 | 10 | @click.command(short_help="Some nostalgia by showing documents some years ago", 11 | help=""" 12 | The memories command shows documents that were 13 | created exactly one (or more..) years ago today. 14 | 15 | \b 16 | Hint: This can be used to send an email to you for 17 | some entertainment. 18 | 19 | """) 20 | @click.option('format', '--format', '-f', 21 | type=click.Choice(['table', 'detail']), default='table', 22 | help='Results by date ending at date') 23 | @click.option('date', '-d', '--date', default=datetime.datetime.now(), 24 | help='Set a custom creation date for document', callback=validate_date) 25 | @click.pass_context 26 | def memories(ctx, format, date): 27 | """ 28 | :format: str 29 | :returns: bool 30 | """ 31 | coll = db.get_document_collection(ctx) 32 | 33 | documents = {} 34 | 35 | limit = 0 36 | utils.log_info("Documents having birthday today.\n") 37 | for year in range(1,40): 38 | start = date - relativedelta(years=year) 39 | start = start.replace(hour=0, minute=0, second=0, microsecond=0) 40 | end = date - relativedelta(years=year,days=-1) 41 | end = end.replace(hour=0, minute=0, second=0, microsecond=0) 42 | 43 | query = {"$and": [ {"created": {'$gte': start, '$lt': end}} ]} 44 | 45 | if format == "table": 46 | docs = coll.find(query) 47 | 48 | for doc in docs: 49 | limit += 1 50 | documents[limit] = doc 51 | 52 | 53 | if format == "detail": 54 | docs = coll.find(query) 55 | for doc in docs: 56 | views.detail(doc) 57 | 58 | # If not detail view - print all memories in one table 59 | if format == "table": 60 | db.clean_shortids(ctx) 61 | for x in reversed(range(1, limit+1)): 62 | db.map_shortid(ctx, sid=x, oid=documents[x]["_id"]) 63 | documents[x]["sid"] = x 64 | 65 | views.table(documents, limit+1) 66 | 67 | 68 | return True 69 | -------------------------------------------------------------------------------- /rvo/commands/modify.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import click 3 | from bson import ObjectId 4 | import rvo.db as db 5 | import rvo.utils as utils 6 | import rvo.transaction as transaction 7 | from rvo.crypto import crypto 8 | from rvo.validate import validate_date 9 | 10 | @click.command(short_help="Modifies a documents metadata", 11 | help=""" 12 | Modifies a documents meta data. 13 | See options for what you can modify. 14 | """) 15 | @click.argument('docid') 16 | @click.option('encrypt', '-e', '--encrypted', type=click.Choice(['yes', 'no', 'unchanged']), 17 | default="unchanged", help='Encrypt this document') 18 | @click.option('tags', '-t', '--tag', 19 | type=str, 20 | multiple=True, 21 | required=False, 22 | show_default=False, 23 | help='Set tags for this document') 24 | @click.option('categories', '-c', '--category', 25 | type=str, 26 | multiple=True, 27 | required=False, 28 | help='Set categories for this document') 29 | @click.option('date', '-d', '--date', default=None, 30 | help='Set a custom creation date for document', callback=validate_date) 31 | @click.pass_context 32 | def modify(ctx, docid, tags, categories, encrypt, date): 33 | """ 34 | Modifies a documents meta data 35 | :docid: str (objectid) 36 | :tags: list 37 | :categories: list 38 | :encrypt: str 39 | :returns: bool 40 | """ 41 | coll = db.get_document_collection(ctx) 42 | d = datetime.datetime.now() 43 | 44 | doc, docid = db.get_document_by_id(ctx, docid) 45 | 46 | # this is a total crazy hack to get 47 | # tuple ("a", "b", "c", " ", "d", "e") 48 | # into 49 | # list ['abc', 'de'] 50 | # tags = ''.join(list(tags)).split() 51 | # categories = ''.join(list(categories)).split() 52 | tags = list(tags) 53 | categories = list(categories) 54 | 55 | if len(tags) > 0: 56 | coll.update({"_id": ObjectId(docid)}, {"$set": {"tags": tags, "updated": d}}) 57 | utils.log_info("Updated tags to %s" % ', '.join(tags)) 58 | transaction.log(ctx, docid, "tags", doc["title"]) 59 | 60 | if len(categories) > 0: 61 | coll.update({"_id": ObjectId(docid)}, {"$set": {"categories": categories, "updated": d}}) 62 | utils.log_info("Updated categories to %s" % ', '.join(categories)) 63 | transaction.log(ctx, docid, "category", doc["title"]) 64 | 65 | if encrypt == "yes": 66 | if doc["encrypted"] is False: 67 | c = crypto(ctx=ctx, password=False) 68 | content = c.encrypt_content(doc["content"].encode("utf-8")) 69 | coll.update({"_id": ObjectId(docid)}, {"$set": {"content": content, "encrypted": True}}) 70 | utils.log_info("Document %s is now stored encrypted" % doc["title"]) 71 | transaction.log(ctx, docid, "encrypted", doc["title"]) 72 | else: 73 | utils.log_error("Document %s is already stored encrypted" % doc["title"]) 74 | 75 | if encrypt == "no": 76 | if doc["encrypted"] is True: 77 | c = crypto(ctx=ctx, password=False) 78 | content = c.decrypt_content(doc["content"]) 79 | coll.update({"_id": ObjectId(docid)}, {"$set": {"content": content, "encrypted": False}}) 80 | utils.log_info("Document %s is now stored in plaintext" % doc["title"]) 81 | transaction.log(ctx, docid, "decrypted", doc["title"]) 82 | else: 83 | utils.log_error("Document %s is already stored in plaintext" % doc["title"]) 84 | 85 | if date != None: 86 | coll.update({"_id": ObjectId(docid)}, {"$set": {"created": date, "updated": d}}) 87 | utils.log_info("Updated creation date to %s" % date) 88 | transaction.log(ctx, docid, "date", doc["title"]) 89 | 90 | return True 91 | -------------------------------------------------------------------------------- /rvo/commands/ping.py: -------------------------------------------------------------------------------- 1 | import click 2 | import rvo.db as db 3 | import rvo.utils as utils 4 | 5 | @click.command(short_help="Verifys the connection to the database", 6 | help=""" 7 | Verifys the connection to the database 8 | and checks read/write access 9 | """) 10 | @click.pass_context 11 | def ping(ctx): 12 | """ 13 | Verifys the connection to the database 14 | and checks read/write access 15 | :docid: string (will be converted to bson object) 16 | :returns: bool 17 | """ 18 | utils.log_info("Trying to connect to database...") 19 | response = db.db_ping(ctx) 20 | if response: 21 | utils.log_info("Connection SUCCESSFUL") 22 | else: 23 | utils.log_error("Connection FAILED") 24 | 25 | response = db.db_verify(ctx) 26 | utils.log_info("Trying to write, read, delete to test_collection...") 27 | if response: 28 | utils.log_info("Interactions were SUCCESSFUL") 29 | else: 30 | utils.log_error("Interactions FAILED") 31 | 32 | if not response: 33 | print("") 34 | utils.log_error("Please check your configuration file") 35 | utils.log_error("You may have:") 36 | utils.log_error("* a syntax error") 37 | utils.log_error("* the connection scheme is wrong") 38 | utils.log_error("* the mongodb instance is not running") 39 | utils.log_error("* the mongodb instance is not reachable") 40 | utils.log_error("* the authentication on mongodb side does not work or is not enabled") 41 | return False 42 | else: 43 | print("") 44 | utils.log_info("Awesome, you can go ahead and use rvo!") 45 | 46 | return True 47 | -------------------------------------------------------------------------------- /rvo/commands/rimport.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import click 3 | import email 4 | import quopri 5 | import pprint 6 | import datetime 7 | from bson import ObjectId 8 | from bson.json_util import dumps 9 | import rvo.db as db 10 | import rvo.utils as utils 11 | import rvo.transaction as transaction 12 | from email.header import decode_header 13 | from rvo.crypto import crypto 14 | from rvo.validate import validate 15 | import rvo.config 16 | 17 | @click.command('import', short_help="Import documents", 18 | help=""" 19 | Import documents. 20 | """) 21 | @click.option('format', '--from', required=False, default="json", 22 | type=click.Choice(['json', 'mail']), 23 | help="Specify the format of the input") 24 | @click.option('-c', '--category', type=str, multiple=True, 25 | help='Import to this category') 26 | @click.option('-t', '--tag', type=str, multiple=True, 27 | help='Import with this tag') 28 | @click.pass_context 29 | def rimport(ctx, format, category, tag): 30 | """ Import to rvo 31 | :returns: bool 32 | """ 33 | 34 | if format == "json": 35 | import_json() 36 | if format == "mail": 37 | import_mail(ctx, tag, category) 38 | 39 | return True 40 | 41 | def import_json(): 42 | print("JSON not implemented yet") 43 | 44 | def import_mail(ctx, tag, category): 45 | content = "" 46 | for l in click.get_text_stream('stdin'): 47 | content = content + l 48 | msg = email.message_from_string(content) 49 | 50 | # title 51 | subject, encoding = email.header.decode_header(msg['Subject'])[0] 52 | if encoding is None: 53 | encoding = "utf-8" 54 | 55 | title = subject.decode(encoding) 56 | 57 | # content 58 | content = msg.get_payload(decode=False) 59 | content = quopri.decodestring(content) 60 | content = "# " + title + '\n\n' + content 61 | date = datetime.datetime.now() 62 | 63 | coll = db.get_document_collection(ctx) 64 | config = ctx.obj["config"] 65 | 66 | item = { 67 | "title": title, 68 | "content": content, 69 | "tags": list(tag), 70 | "categories": list(category), 71 | "created": date, 72 | "updated": date, 73 | "encrypted": False, 74 | } 75 | 76 | # insert item if its valid 77 | if validate(item): 78 | coll = db.get_document_collection(ctx) 79 | docid = coll.insert_one(item).inserted_id 80 | 81 | transaction.log(ctx, str(docid), "import", title) 82 | utils.log_info("Document \"%s\" created." % title) 83 | 84 | else: 85 | utils.log_error("Validation of the updated object did not succeed") 86 | -------------------------------------------------------------------------------- /rvo/commands/show.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import click 3 | from bson import ObjectId 4 | import rvo.db as db 5 | import rvo.utils as utils 6 | import rvo.transaction as transaction 7 | from rvo.crypto import crypto 8 | from rvo.validate import validate 9 | import rvo.config 10 | 11 | @click.command(short_help="Shows a document", 12 | help=""" 13 | Shows a single document from documentstore 14 | and opens its content in a pager (or stdout). 15 | """) 16 | @click.argument('docid') 17 | @click.option('password', '-p', '--password', required=False, default=False, 18 | help="Password for encrypted documents") 19 | @click.option('stdout', '-s', '--stdout', required=False, default=False, is_flag=True, 20 | help="Print document to stdout instead of opening pager") 21 | @click.pass_context 22 | def show(ctx, docid, password, stdout): 23 | """ 24 | Shows a single object from database 25 | and opens its content in a pager 26 | :docid: string (will be converted to bson object) 27 | :returns: bool 28 | """ 29 | 30 | coll = db.get_document_collection(ctx) 31 | doc, docid = db.get_document_by_id(ctx, docid) 32 | 33 | config = ctx.obj["config"] 34 | 35 | content, c = db.get_content(ctx, doc, password=password) 36 | 37 | if sys.stdout.isatty() and not stdout: 38 | utils.view_content_in_pager(config["pager"], config['pageropts'], template=content) 39 | else: 40 | # try: 41 | # print(content.encode("utf-8")) 42 | # except UnicodeDecodeError: 43 | print(content.encode("utf-8")) 44 | 45 | transaction.log(ctx, docid, "show", doc["title"]) 46 | 47 | # showing the message means decrypting the message 48 | # in order to do not reuse the nonce from salsa20, 49 | # we have to reencrypt the content and update the field 50 | if doc["encrypted"] is True: 51 | doc["content"] = c.encrypt_content(content.encode("utf-8")) 52 | if validate(doc): 53 | coll.save(doc) 54 | else: 55 | utils.log_error("Validation of the updated object did not succeed") 56 | 57 | 58 | return True 59 | 60 | -------------------------------------------------------------------------------- /rvo/commands/stats.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import click 4 | import datetime 5 | from dateutil.relativedelta import relativedelta 6 | import rvo.db as db 7 | import rvo.utils as utils 8 | import rvo.views as views 9 | from rvo.cli import validate_date 10 | import rvo.analysis as analysis 11 | from rvo.crypto import crypto 12 | from rvo.validate import validate 13 | from tabulate import tabulate 14 | import rvo.config 15 | 16 | @click.command(short_help="Text analysis on a document", 17 | help=""" 18 | To be honest, I just wanted to play with nltk. 19 | Also i like statistics and this kind of stuff. 20 | 21 | So this will print out some natrual language facts about 22 | the document given. 23 | """) 24 | @click.option('password', '-p', '--password', required=False, default=False, 25 | help="Password for encrypted documents") 26 | @click.option('-c', '--category', type=str, multiple=True, 27 | help='filter for category') 28 | @click.option('-t', '--tag', type=str, multiple=True, 29 | help='filter for tags') 30 | @click.option('objectid', '-i', '--id', type=str, default=False, 31 | help='only on a single document by id') 32 | @click.pass_context 33 | def stats(ctx, objectid, category, tag, password): 34 | """ 35 | :docid: str 36 | :returns: bool 37 | """ 38 | 39 | coll = db.get_document_collection(ctx) 40 | 41 | if objectid: 42 | docs = [] 43 | doc, docid = db.get_document_by_id(ctx, str(objectid)) 44 | docs.append(doc) 45 | else: 46 | tags = utils.normalize_element(tag, "tags") 47 | categories = utils.normalize_element(category, "categories") 48 | query = {"$and": [ tags, categories ] } 49 | coll = db.get_document_collection(ctx) 50 | 51 | config = ctx.obj["config"] 52 | 53 | docs = coll.find(query).sort("updated", -1) 54 | 55 | content = "" 56 | tags = [] 57 | categories = [] 58 | c = False 59 | for doc in docs: 60 | if c is False: 61 | doc["content"],c = db.get_content(ctx, doc, crypto_object=c, password=password) 62 | 63 | content += doc["content"] 64 | tags.extend(doc["tags"]) 65 | categories.extend(doc["categories"]) 66 | 67 | if len(content) == 0: 68 | utils.log_error("No documents found with this query") 69 | return False 70 | 71 | 72 | utils.log_info("Text analysis\n") 73 | table = [] 74 | headers = ["Analysis", "Result"] 75 | 76 | try: 77 | analysis.get_sentences("this is a test") 78 | except LookupError: 79 | utils.log_error("NLTK is needed to do text analysis on your document") 80 | utils.log_error("In order to do this, execute:") 81 | utils.log_error(' python -c \'import nltk; nltk.download("book")\'') 82 | sys.exit(1) 83 | 84 | table.append(["Sentences" , analysis.get_sentences(content)]) 85 | table.append(["Words", analysis.get_words(content)]) 86 | table.append(["Characters", analysis.get_characters(content)]) 87 | table.append(["Tags", len(tags)]) 88 | table.append(["Categories", len(categories)]) 89 | table.append(["Size of documents", analysis.get_size_of_document(content)]) 90 | if objectid: 91 | table.append(["Age of document", analysis.get_age_of_document(doc["created"])]) 92 | 93 | cwords = "" 94 | fdist = analysis.get_word_distribution(content) 95 | 96 | for w, f in analysis.get_most_common_words(fdist, 5): 97 | cwords = cwords + w + "(" + str(f) + ") " 98 | table.append(["Most common words", cwords]) 99 | 100 | lwords = "" 101 | for f in analysis.get_least_common_words(fdist, 5): 102 | lwords = lwords + " " + f 103 | table.append(["Least common words", lwords]) 104 | table.append(["Words per sentence", analysis.get_words_per_sentence(content)]) 105 | 106 | longwords = "" 107 | for f in analysis.get_long_words(fdist, 5): 108 | longwords = longwords + " " + f 109 | table.append(["Long words", longwords]) 110 | 111 | print tabulate(table, headers=headers) 112 | print("") 113 | 114 | return True 115 | -------------------------------------------------------------------------------- /rvo/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ConfigParser 3 | import rvo.utils as utils 4 | 5 | def parse_config(conf="~/.rvo.conf"): 6 | 7 | # soon.... 8 | default = {} 9 | con = dict() 10 | config = ConfigParser.ConfigParser() 11 | 12 | if not config.read(os.path.expanduser(conf)): 13 | utils.log_error("Error: your con %s could not be read" % conf) 14 | exit(1) 15 | 16 | try: 17 | con["uri"] = config.get("General", "MongoDB") 18 | except ConfigParser.NoOptionError: 19 | utils.log_error("Error: Please set a MongoDB Connection URI in %s" % conf) 20 | exit(1) 21 | 22 | try: 23 | con["db"] = config.get("General", "DB") 24 | except ConfigParser.NoOptionError: 25 | con["db"] = "rvo" 26 | 27 | try: 28 | con["editor"] = config.get("General", "Editor") 29 | except ConfigParser.NoOptionError: 30 | con["editor"] = "vim" 31 | 32 | try: 33 | con["pager"] = config.get("General", "Pager") 34 | except ConfigParser.NoOptionError: 35 | con["pager"] = "less" 36 | 37 | try: 38 | con["pageropts"] = config.get("General", "PagerOptions") 39 | except ConfigParser.NoOptionError: 40 | con["pageropts"] = None 41 | 42 | try: 43 | con["mailfrom"] = config.get("General", "MailFrom") 44 | except ConfigParser.NoOptionError: 45 | con["mailfrom"] = "nobody@example.net" 46 | 47 | con["collection"] = "documents" 48 | con["shortids"] = "cache" 49 | con["transactions"] = "transactions" 50 | con["config"] = "config" 51 | 52 | return con 53 | 54 | -------------------------------------------------------------------------------- /rvo/crypto.py: -------------------------------------------------------------------------------- 1 | """ 2 | crypto is used for encryption and decryption 3 | the documents. 4 | 5 | init_master and get_master are there for an additional 6 | layer (indirection). Your password only encrypts the 7 | master key once. 8 | 9 | Every document is later encrypted and decrypted by the master 10 | key that was generated once. 11 | """ 12 | import sys 13 | import nacl.secret 14 | import nacl.utils 15 | import click 16 | from pyblake2 import blake2b 17 | import utils 18 | import rvo.db as db 19 | 20 | class crypto(object): 21 | """ The crypto class for everything rvo needs. """ 22 | 23 | def __init__(self, ctx, password=False): 24 | self.collection = db.get_config_collection(ctx) 25 | self.ctx = ctx 26 | 27 | r = self.collection.find_one({"masterkey": {"$exists": True}}) 28 | if r is None: 29 | self.init_master() 30 | 31 | self._masterkey = self.get_master(password) 32 | 33 | 34 | def init_master(self): 35 | """ 36 | Generates the master key to encrypt all documents later 37 | Will ask for password to initialize and will put into the collection config 38 | :returns: bool 39 | """ 40 | 41 | # Configuration collection 42 | coll = self.collection 43 | 44 | # generate random 256 byte to use as plain masterkey 45 | masterkey = blake2b(digest_size=32) 46 | masterkey.update(nacl.utils.random(256)) 47 | masterkey = masterkey.digest() 48 | 49 | # ask for password to de/encrypt the masterkey 50 | key = blake2b(digest_size=32) 51 | utils.log_info("Encryption has not been initialized yet. Please enter your password.") 52 | pw = click.prompt("%s Set Password" % utils.query_prefix, type=str, hide_input=True, confirmation_prompt=True) 53 | key.update(pw.encode("utf-8")) 54 | key = key.digest() 55 | utils.log_info("Password set successfully. Dont forget it, otherwise you are fucked.") 56 | 57 | # encrypt masterkey with user password 58 | box = nacl.secret.SecretBox(key) 59 | nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) 60 | enc = box.encrypt(masterkey, nonce, encoder=nacl.encoding.HexEncoder) 61 | 62 | r = coll.insert({"masterkey": enc}) 63 | return True 64 | 65 | def get_master(self, password): 66 | """ 67 | Will be triggered everytime the user tries to access an encrypted 68 | document from the database. Asks for his password, decrypts master key 69 | and returns the masterkey what will be an attribute from the crypto class 70 | :returns: str 71 | """ 72 | # Fetch encrypted masterkey from db 73 | coll = self.collection 74 | masterkey = coll.find_one({"masterkey": {"$exists": True}}) 75 | masterkey = masterkey["masterkey"] 76 | 77 | # hash input pw 78 | key = blake2b(digest_size=32) 79 | 80 | if password is False: 81 | password = click.prompt("%s Password" % utils.query_prefix, type=str, hide_input=True) 82 | 83 | key.update(password.encode("utf-8")) 84 | key = key.digest() 85 | 86 | 87 | # init box 88 | box = nacl.secret.SecretBox(key) 89 | 90 | # use password to decrypt masterkey 91 | try: 92 | masterkey = box.decrypt(ciphertext=masterkey, encoder=nacl.encoding.HexEncoder) 93 | return masterkey 94 | except nacl.exceptions.CryptoError: 95 | utils.log_error("Invalid Password") 96 | sys.exit(1) 97 | return False 98 | 99 | 100 | def encrypt_content(self, content): 101 | """ 102 | Will encrypt the content that is given with the masterkey from crypto class 103 | :content: str (plaintext) 104 | :returns: str (encrypted and encoded in hexdigest) 105 | """ 106 | 107 | # init new blake2b keystore 108 | # and get password as hex 109 | try: 110 | key = blake2b(digest_size=32) 111 | key.update(self._masterkey) 112 | key = key.digest() 113 | except TypeError: 114 | sys.exit(1) 115 | box = nacl.secret.SecretBox(key) 116 | 117 | # generate a nonce 118 | nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) 119 | 120 | try: 121 | msg = box.encrypt(content, nonce, encoder=nacl.encoding.HexEncoder) 122 | return msg 123 | except nacl.exceptions.CryptoError: 124 | utils.log_error("Invalid Password") 125 | return False 126 | 127 | def decrypt_content(self, content): 128 | """ 129 | Will decrypt the content that is given as argument using the 130 | masterkey from crypto class. 131 | :content: str (encrypted and encoded in hexdigest) 132 | :returns: str (plaintext) 133 | """ 134 | 135 | # init new blake2b keystore and hash 136 | # masterkey 137 | try: 138 | key = blake2b(digest_size=32) 139 | key.update(self._masterkey) 140 | key = key.digest() 141 | except TypeError: 142 | sys.exit(1) 143 | 144 | box = nacl.secret.SecretBox(key) 145 | 146 | content = content.encode("utf-8") 147 | 148 | try: 149 | plain = box.decrypt(ciphertext=content, encoder=nacl.encoding.HexEncoder) 150 | return plain 151 | except nacl.exceptions.CryptoError: 152 | utils.log_error("Invalid Password") 153 | return False 154 | -------------------------------------------------------------------------------- /rvo/db.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pymongo 3 | import bson 4 | from bson.objectid import ObjectId 5 | import rvo.utils as utils 6 | import rvo.config 7 | 8 | def db_ping(ctx): 9 | """ Ping the connection to mongodb database 10 | :returns: bool 11 | """ 12 | try: 13 | config = ctx.obj["config"] 14 | c = ctx.obj["db"] 15 | c.server_info() 16 | return True 17 | except: 18 | return False 19 | 20 | def db_verify(ctx): 21 | """ Does a test write, read and remove 22 | :returns: bool 23 | """ 24 | try: 25 | # Define name 26 | testcoll = "test_collection" 27 | 28 | # Fetch config and get db object 29 | config = ctx.obj["config"] 30 | c = ctx.obj["db"] 31 | db = c[config["db"]] 32 | 33 | # Get collection object 34 | coll = get_collection(ctx, testcoll) 35 | 36 | # Do write test 37 | coll.insert({"a": 1}) 38 | 39 | # Do read test 40 | resp = coll.find_one({}) 41 | if resp == {"a": 1}: 42 | pass 43 | 44 | # Do removal test 45 | coll.remove({}) 46 | 47 | # Drop test_collection 48 | db.drop_collection(testcoll) 49 | 50 | return True 51 | except: 52 | return False 53 | 54 | def get_collection(ctx, coll): 55 | """ Initialize MongoDB Connection 56 | :returns: collection object 57 | """ 58 | config = ctx.obj["config"] 59 | c = ctx.obj["db"] 60 | db = c[config["db"]] 61 | collection = db[coll] 62 | return collection 63 | 64 | def get_document_collection(ctx): 65 | """ 66 | Get document collection handle 67 | from config. 68 | :returns: collection object 69 | """ 70 | config = ctx.obj["config"] 71 | collection = get_collection(ctx, config["collection"]) 72 | return collection 73 | 74 | def get_transactions_collection(ctx): 75 | """ 76 | Get document collection handle 77 | from config. 78 | :returns: collection object 79 | """ 80 | config = ctx.obj["config"] 81 | collection = get_collection(ctx, config["transactions"]) 82 | return collection 83 | 84 | def get_config_collection(ctx): 85 | """ 86 | Get document collection handle 87 | from config. 88 | :returns: collection object 89 | """ 90 | config = ctx.obj["config"] 91 | collection = get_collection(ctx, config["config"]) 92 | return collection 93 | 94 | def get_shortids_collection(ctx): 95 | """ 96 | Get document collection handle 97 | from config. 98 | :returns: collection object 99 | """ 100 | config = ctx.obj["config"] 101 | collection = get_collection(ctx, config["shortids"]) 102 | return collection 103 | 104 | def check_for_duplicate(ctx, field, content): 105 | """ Check if url already exists in db 106 | :returns: bool 107 | """ 108 | config = ctx.obj["config"] 109 | coll = get_collection(ctx, config["collection"]) 110 | 111 | if coll.find({field: content}).count() > 0: 112 | return True 113 | else: 114 | return False 115 | 116 | def get_document_by_id(ctx, id): 117 | """ 118 | Gets a document by its id, exits if false 119 | :coll: pymongo collection object 120 | :id: str 121 | :returns: dict, docid 122 | """ 123 | coll = get_document_collection(ctx) 124 | shortids = get_shortids_collection(ctx) 125 | 126 | try: 127 | doc = shortids.find_one({"sid": int(id)}) 128 | id = str(doc["oid"]) 129 | except (IndexError, ValueError, TypeError) as e: 130 | pass 131 | 132 | try: 133 | doc = coll.find_one({"_id": ObjectId(id)}) 134 | if doc is None: 135 | utils.log_info("No Results for %s" % id) 136 | sys.exit(1) 137 | return doc, id 138 | except bson.errors.InvalidId: 139 | utils.log_error("Error: %s is not a valid ID or ObjectId." % id) 140 | sys.exit(1) 141 | 142 | def map_shortid(ctx, sid, oid): 143 | """ 144 | Maps oid to a short id for better usage 145 | in commandline. At the end you have to type 146 | "edit 2" and not "edit 56823D28ds23821" 147 | Mapping will be stored in a separate collection 148 | and should be resettet with each view. 149 | :coll: pymongo collection object 150 | :sid: str (bson object id) 151 | :oid: str (bson object id) 152 | :returns: dict 153 | """ 154 | shortids = get_shortids_collection(ctx) 155 | shortids.insert({"sid": sid, "oid": oid}) 156 | return True 157 | 158 | def clean_shortids(ctx): 159 | """ 160 | Cleans all shortids from cache collection. 161 | Necessary to get fresh results for every search 162 | :coll: pymongo collection object 163 | :returns: bool 164 | """ 165 | shortids = get_shortids_collection(ctx) 166 | shortids.remove({"$and": [{"sid": {"$exists": True}}, {"oid": {"$exists": True}}]}) 167 | return True 168 | 169 | def add_transaction(ctx, item): 170 | transactions = get_transactions_collection(ctx) 171 | transactions.insert(item) 172 | return True 173 | 174 | def get_content(ctx, doc, crypto_object=False, password=False): 175 | """ 176 | Get content of a document. 177 | For later use, it also returns the initialized crypto object 178 | :doc: document object 179 | :crypto_object: instance of class rvo.crypto.crypto 180 | returns: str, crypto_object 181 | """ 182 | from rvo.crypto import crypto 183 | c = crypto_object 184 | if doc["encrypted"] is True: 185 | if c is False: 186 | c = crypto(ctx=ctx, password=password) 187 | content = c.decrypt_content(doc["content"]) 188 | content = content.decode("utf8") 189 | else: 190 | content = doc["content"] 191 | 192 | return content, c 193 | -------------------------------------------------------------------------------- /rvo/transaction.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import rvo.db as db 3 | 4 | def log(ctx, document, type, title): 5 | """ 6 | Logs any interaction with rvo to the database 7 | One action generates exactly one entry in the transactions 8 | collection. The purpose is to be able to track changes 9 | and see what happens. 10 | :document: str (objectid) 11 | :type: str (show, mail, modify, info, add, edit) 12 | :message: 13 | :returns: bool 14 | """ 15 | d = datetime.datetime.now() 16 | item = { 17 | "document": document, 18 | "date": d, 19 | "type": type, 20 | "title": title 21 | } 22 | db.add_transaction(ctx, item) 23 | 24 | -------------------------------------------------------------------------------- /rvo/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import os 4 | import tempfile 5 | import subprocess 6 | import urllib2 7 | import ssl 8 | import click 9 | from bs4 import BeautifulSoup 10 | 11 | # Content utils 12 | 13 | def get_content_from_editor(config, template=""): 14 | """ 15 | Writes content to file and opens up the editor that is 16 | parameter config. Returns new content and deletes file 17 | :config: str (commands) 18 | :template: str 19 | :returns: bool 20 | """ 21 | _, tmpfile = tempfile.mkstemp(prefix="rvo-", text=True, suffix=".md") 22 | with open(tmpfile, 'w') as f: 23 | if template: 24 | try: 25 | f.write(template.encode("utf-8")) 26 | except (UnicodeEncodeError, UnicodeDecodeError) as e: 27 | f.write(template) 28 | subprocess.call(config.split() + [tmpfile]) 29 | with open(tmpfile, "r") as f: 30 | raw = f.read() 31 | if not raw: 32 | log_error("Error: No content from editor") 33 | sys.exit(1) 34 | return raw, tmpfile 35 | 36 | def view_content_in_pager(config, opts, template=""): 37 | """ 38 | Writes content to file and opens up the pager that is 39 | parameter config. Temp file will be deleted afterwards. 40 | :config: str (commands) 41 | :opts: str (pager options for commandline) 42 | :template: str 43 | :returns: bool 44 | """ 45 | _, tmpfile = tempfile.mkstemp(prefix="rvo-", text=True, suffix=".md") 46 | with open(tmpfile, 'w') as f: 47 | if template: 48 | try: 49 | f.write(template.encode("utf-8")) 50 | except (UnicodeEncodeError, UnicodeDecodeError) as e: 51 | f.write(template) 52 | 53 | if opts is not None: 54 | subprocess.call(config.split() + [opts] + [tmpfile]) 55 | else: 56 | subprocess.call(config.split() + [tmpfile]) 57 | 58 | os.remove(tmpfile) 59 | return True 60 | 61 | def get_title_from_webpage(url): 62 | """ Fetch of a html site for title element 63 | :url: str (http url) 64 | :returns: str 65 | """ 66 | 67 | # LOL SECURITY 68 | ssl._create_default_https_context = ssl._create_unverified_context 69 | 70 | try: 71 | h = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'} 72 | u = urllib2.Request(url, headers=h) 73 | u = urllib2.urlopen(u) 74 | soup = BeautifulSoup(u, "html.parser") 75 | s = soup.title.string.replace('\n', ' ').replace('\r', '').lstrip().rstrip() 76 | s = s.lstrip() 77 | return s 78 | except (AttributeError, MemoryError, ssl.CertificateError, IOError) as e: 79 | return "No title" 80 | except ValueError: 81 | return False 82 | 83 | def get_title_from_content(content): 84 | """ 85 | Generates a title from the content 86 | using the first line and stripping away whitespace 87 | and hash signs 88 | :content: str 89 | :returns: str 90 | 91 | """ 92 | title = content.split('\n', 1)[0].replace('#', '').lstrip()[0:50] 93 | return title 94 | 95 | def clean_tmpfile(tmpfile): 96 | """ 97 | Basically, this removes a file. Its used to only 98 | delete the file that the editor leaves behind in case that 99 | rvo crashes. 100 | """ 101 | try: 102 | os.remove(tmpfile) 103 | except OSError: 104 | pass 105 | return True 106 | 107 | # Styling 108 | 109 | query_prefix = '\033[38;5;154m>\033[38;5;118m>\033[38;5;120m>\033[0m' 110 | 111 | def log_error(msg): 112 | """ 113 | Error output on terminal 114 | """ 115 | prefix = '\033[38;5;196m>\033[38;5;202m>\033[38;5;208m>\033[0m' 116 | click.echo(prefix + " " + msg) 117 | 118 | def log_info(msg): 119 | """ 120 | Info output on terminal 121 | """ 122 | prefix = '\033[38;5;129m>\033[38;5;135m>\033[38;5;141m>\033[0m' 123 | click.echo(prefix + " " + msg) 124 | 125 | # OS and Testing Utils 126 | 127 | def isatty(): 128 | """ 129 | Wrapper for isatty 130 | I use this for monkeypatching input detection 131 | in the add method. Its not nice. It works. Its mine <3 132 | """ 133 | return sys.stdin.isatty() 134 | 135 | # Transformation of elements 136 | 137 | def normalize_element(filter, field): 138 | """ 139 | Turn items into a dict and apply regexes 140 | """ 141 | 142 | if len(filter) > 0: 143 | filters = [] 144 | for f in filter: 145 | filters.append(re.compile(f, re.IGNORECASE)) 146 | filters = {field: { "$in": filters}} 147 | else: 148 | filters = {} 149 | 150 | return filters 151 | 152 | def remove_emojis(content): 153 | """ 154 | Removes emojis from a string 155 | :content: str 156 | :returns: str 157 | """ 158 | try: 159 | content = unicode(content) 160 | except UnicodeDecodeError: 161 | pass 162 | 163 | try: 164 | emojis = re.compile(u'[' 165 | u'\U0001F300-\U0001F64F' 166 | u'\U0001F680-\U0001F6FF' 167 | u'\u2600-\u26FF\u2700-\u27BF]+', 168 | re.UNICODE) 169 | except re.error: 170 | emojis = re.compile(u'(' 171 | u'\ud83c[\udf00-\udfff]|' 172 | u'\ud83d[\udc00-\ude4f\ude80-\udeff]|' 173 | u'[\u2600-\u26FF\u2700-\u27BF])+', 174 | re.UNICODE) 175 | 176 | return emojis.sub('', content) # no emoji 177 | -------------------------------------------------------------------------------- /rvo/validate.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from geopy.geocoders import Nominatim 3 | from dateutil.parser import parse 4 | 5 | # validators 6 | def validate_date(ctx, param, value): 7 | try: 8 | value = parse(value) 9 | return value 10 | except (AttributeError, TypeError) as e: 11 | return value 12 | except ValueError: 13 | raise click.BadParameter('Date format \"%s\" not valid' % value) 14 | 15 | def validate_location(ctx, param, value): 16 | 17 | if value is None: 18 | return value 19 | 20 | geolocator = Nominatim() 21 | location = geolocator.geocode(value) 22 | 23 | if location is None: 24 | raise click.BadParameter('Location \"%s\" could not be found' % value) 25 | 26 | return location 27 | 28 | def validate(document): 29 | 30 | fields = [ 31 | "title", 32 | "encrypted", 33 | "content", 34 | "created", 35 | "updated", 36 | "tags", 37 | "categories" 38 | ] 39 | 40 | # Check if we have an document 41 | if not isinstance(document, dict): 42 | return False 43 | 44 | # Verifiy if all fields are existent 45 | try: 46 | for field in fields: 47 | document[field] 48 | except KeyError: 49 | return False 50 | 51 | # Check if content is utf8 52 | if not isinstance(document["content"], str): 53 | if not isinstance(document["content"], unicode): 54 | return False 55 | 56 | # # Check title utf8 57 | if not isinstance(document["title"], str): 58 | if not isinstance(document["title"], unicode): 59 | return False 60 | 61 | # Check if tags is a list 62 | if not isinstance(document["tags"], list): 63 | return False 64 | 65 | # Check if categories is a list 66 | if not isinstance(document["categories"], list): 67 | return False 68 | 69 | # Check if create is a valid datetime object 70 | if not isinstance(document["created"], datetime.datetime): 71 | return False 72 | 73 | # Check if updated is a valid datetime object 74 | if not isinstance(document["updated"], datetime.datetime): 75 | return False 76 | 77 | if not isinstance(document["encrypted"], bool): 78 | return False 79 | 80 | return True 81 | -------------------------------------------------------------------------------- /rvo/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | """ 3 | Views are the rendering part. A dict object (most likly from mongodb) 4 | will be displayed in a human readable, colorized way 5 | """ 6 | from tabulate import tabulate 7 | 8 | def colorize(ty, msg): 9 | """ 10 | Will colorize a given message using 11 | a color scheme (prompt) of a given type. 12 | :ty: str (type of colorizations) 13 | :msg: str (content to colorize) 14 | :returns: str (full colorized) 15 | """ 16 | 17 | reset = '\033[0m' 18 | if ty == "title": 19 | pre = '\033[38;5;118m' 20 | if ty == 'tags': 21 | pre = '\033[38;5;97m' 22 | if ty == 'cats': 23 | pre = '\033[38;5;121m' 24 | if ty == 'header': 25 | pre = '\033[38;5;15m' 26 | 27 | return pre + msg + reset 28 | 29 | def display_tags(tags): 30 | """ 31 | Display tags if they are available 32 | :tags: list 33 | :returns: bool 34 | """ 35 | print("tags:"), 36 | for e in tags: 37 | print(colorize(msg=e,ty="tags")), 38 | return True 39 | 40 | def display_categories(cat): 41 | """ 42 | Display categories if they are available 43 | :cat: list 44 | :returns: bool 45 | """ 46 | print(" cats:"), 47 | for e in cat: 48 | print(colorize(msg=e,ty="cats")), 49 | return True 50 | 51 | def table(documents, size): 52 | # print documents 53 | table = [] 54 | headers = [ 55 | colorize(msg="ID", ty="header"), 56 | colorize(msg="Title", ty="header"), 57 | colorize(msg="Cats", ty="header"), 58 | colorize(msg="Tags", ty="header"), 59 | colorize(msg="Date", ty="header") 60 | ] 61 | 62 | for x in reversed(range(1,size)): 63 | 64 | y = [ 65 | documents[x]["sid"], 66 | colorize(msg=documents[x]["title"][0:60], ty="title"), 67 | colorize(msg=' '.join(documents[x]["categories"]), ty="cats"), 68 | colorize(msg=' '.join(documents[x]["tags"]),ty="tags"), 69 | documents[x]["created"].strftime("%Y-%m-%d") 70 | ] 71 | table.append(y) 72 | 73 | print tabulate(table, headers=headers) 74 | print "" 75 | 76 | def detail(doc): 77 | """ 78 | Display a single object using eventually meta 79 | wrappers from above with all meta data that exist. 80 | :r: dict (document from database) 81 | :returns: bool 82 | """ 83 | 84 | # ID 85 | print("") 86 | print(" id: " + str(doc["_id"])) 87 | 88 | # Title 89 | print("title:"), 90 | print(colorize(msg=doc["title"],ty="title")) 91 | print(""), 92 | 93 | # Tags 94 | try: 95 | display_tags(doc["tags"]) 96 | except KeyError: 97 | pass 98 | print("") 99 | 100 | # Categories 101 | try: 102 | display_categories(doc["categories"]) 103 | except KeyError: 104 | pass 105 | print("") 106 | 107 | # Date 108 | print(" date: " + doc["created"].strftime('%Y-%m-%d %H:%M')) 109 | print(" mod: " + doc["updated"].strftime('%Y-%m-%d %H:%M')) 110 | print(" enc: " + str(doc["encrypted"])) 111 | 112 | # URL 113 | if any("links" in s for s in doc["categories"]): 114 | print(" url: " + doc["content"]) 115 | 116 | print("") 117 | 118 | return True 119 | 120 | def transactions(documents, size): 121 | """ 122 | Display a single object using eventually meta 123 | wrappers from above 124 | :r: dict (document from database) 125 | :returns: bool 126 | """ 127 | 128 | # print documents 129 | table = [] 130 | headers = [ 131 | colorize(msg="Date", ty="header"), 132 | colorize(msg="Type", ty="header"), 133 | colorize(msg="Title", ty="header"), 134 | colorize(msg="ObjectId", ty="header"), 135 | ] 136 | 137 | for x in reversed(range(1,size)): 138 | 139 | y = [ 140 | documents[x]["date"].strftime("%Y-%m-%d %H:%M"), 141 | colorize(msg=documents[x]["type"], ty="cats"), 142 | colorize(msg=documents[x]["title"], ty="title"), 143 | str(documents[x]["_id"]), 144 | ] 145 | table.append(y) 146 | 147 | print tabulate(table, headers=headers) 148 | print "" 149 | 150 | return True 151 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | # To use a consistent encoding 4 | from codecs import open 5 | import os 6 | import sys 7 | 8 | # file read helper 9 | def read_from_file(path): 10 | if os.path.exists(path): 11 | with open(path,"rb","utf-8") as input: 12 | return input.read() 13 | 14 | version = "23.0.6" 15 | 16 | setup( 17 | name='rvo', 18 | version=version, 19 | description='Managing text data from the commandline', 20 | long_description=read_from_file('README.rst'), 21 | url='https://github.com/noqqe/rvo', 22 | author='Florian Baumann', 23 | author_email='flo@noqqe.de', 24 | license='MIT', 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Environment :: Console', 28 | 'Operating System :: POSIX :: BSD :: OpenBSD', 29 | 'Topic :: Terminals', 30 | 'Topic :: Utilities', 31 | 'Topic :: Documentation', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Programming Language :: Python :: 2.7', 34 | ], 35 | keywords='links mongodb quotes notes journal diary', 36 | packages=find_packages(), 37 | zip_safe=True, 38 | install_requires=['pymongo', 'configparser', 'BeautifulSoup4', 39 | 'pynacl', 'pyblake2', 'tabulate', 'click', 40 | 'python-dateutil', 'python-simplemail', 'nltk', 41 | 'hurry.filesize', 'geopy'], 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'rvo=rvo.cli:cli', 45 | ], 46 | }, 47 | ) 48 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import sys 4 | import pytest 5 | from click.testing import CliRunner 6 | from rvo import cli 7 | import rvo.commands.add 8 | import rvo.commands.list 9 | import datetime 10 | import rvo.config 11 | import mongomock 12 | from bson import ObjectId 13 | from dateutil import parser 14 | 15 | def mock_documents_collection(ctx): 16 | 17 | docs = [] 18 | 19 | docs.append({ 20 | "_id" : ObjectId("569e5eed6815b47ce7bdb583"), 21 | "updated" : parser.parse("2015-02-01"), 22 | "encrypted" : False, 23 | "created" : parser.parse("2015-02-01"), 24 | "content" : "# Nutella, Coffee, ninja\n\nNutrlla, Horst, hadoop\n", 25 | "title" : "Nutella, Coffee, ninja", 26 | "categories" : [ "notes" ], 27 | "tags" : [ "list", "todo" ] }) 28 | docs.append({ 29 | "_id" : ObjectId("569e5eed6815b47ce7bdb584"), 30 | "updated" : parser.parse("2016-03-05"), 31 | "encrypted" : False, 32 | "created" : parser.parse("2016-03-01"), 33 | "content" : "just broke my leg\n", 34 | "title" : "Whoop Whoop", 35 | "categories" : [ "jrnl" ], 36 | "tags" : [ "holiday", "vacation" ] }) 37 | docs.append({ 38 | "_id" : ObjectId("569e5eed6815b47ce7bdb585"), 39 | "updated" : parser.parse("2004-04-04"), 40 | "encrypted" : False, 41 | "created" : parser.parse("2004-04-01"), 42 | "content" : "just broke my leg again\n", 43 | "title" : "Whoop Whoop Reloaded", 44 | "categories" : [ "jrnl" ], 45 | "tags" : [ "holiday", "vacation" ] }) 46 | docs.append({ 47 | "_id" : ObjectId("569e5eed6815b47ce7bdb586"), 48 | "updated" : parser.parse("1989-06-15"), 49 | "encrypted" : False, 50 | "created" : parser.parse("1989-06-15"), 51 | "content" : "new to python tests\n", 52 | "title" : "How to get in touch with mocks", 53 | "categories" : [ "docs" ], 54 | "tags" : [ "python", "mock", "tests" ] }) 55 | docs.append({ 56 | "_id" : ObjectId("569e5eed6815b47ce7bdb587"), 57 | "updated" : parser.parse("2016-03-05"), 58 | "encrypted" : True, 59 | "created" : parser.parse("2016-03-05"), 60 | # plaintext: "pynacl is just great" 61 | "content" : "8d55029288e4421b1770c1335f8d1de433b6615106cf1c709c9d73f6b45c2bef2a7d4d6766615a1742e987d458441a69f7ac85750c22781a088eb199", 62 | "title" : "This is an encrypted entry", 63 | "categories" : [ "crypto" ], 64 | "tags" : [ "pyblake2", "crypto" ] }) 65 | c = mongomock.MongoClient().db.collection 66 | 67 | for doc in docs: 68 | c.insert(doc) 69 | return c 70 | 71 | def mock_shortids_collection(ctx): 72 | 73 | docs = [] 74 | 75 | docs.append({ 76 | "_id" : ObjectId("569e5eed6815b47ce8bdb583"), 77 | "oid" : ObjectId("569e5eed6815b47ce7bdb583"), 78 | "sid": 1}, 79 | ) 80 | docs.append({ 81 | "_id" : ObjectId("569e5eed6815b47ce8bdb584"), 82 | "oid" : ObjectId("569e5eed6815b47ce7bdb584"), 83 | "sid": 2}, 84 | ) 85 | docs.append({ 86 | "_id" : ObjectId("569e5eed6815b47ce8bdb585"), 87 | "oid" : ObjectId("569e5eed6815b47ce7bdb585"), 88 | "sid": 3}, 89 | ) 90 | docs.append({ 91 | "_id" : ObjectId("569e5eed6815b47ce8bdb586"), 92 | "oid" : ObjectId("569e5eed6815b47ce7bdb586"), 93 | "sid": 4}, 94 | ) 95 | docs.append({ 96 | "_id" : ObjectId("569e5eed6815b47ce8bdb587"), 97 | "oid" : ObjectId("569e5eed6815b47ce7bdb587"), 98 | "sid": 5}, 99 | ) 100 | 101 | c = mongomock.MongoClient().db.collection 102 | 103 | for doc in docs: 104 | c.insert(doc) 105 | return c 106 | 107 | def mock_config_collection(ctx): 108 | 109 | docs = [] 110 | 111 | # pw is test123 112 | docs.append({ 113 | "_id": ObjectId("569e5eed6815b47ce9bdb583"), 114 | "masterkey": "90312727630970024125495afc71b5e111f2f375a140fa8450e3fd1ec4e947260ab22e960f5b32f75647bbb4ead88d42834c3099232fd8e10fe8838e56e5147e8a87dcfb251676a1", 115 | }) 116 | 117 | c = mongomock.MongoClient().db.collection 118 | 119 | for doc in docs: 120 | c.insert(doc) 121 | return c 122 | 123 | 124 | def mock_transactions_collection(ctx): 125 | 126 | docs = [] 127 | 128 | docs.append({ 129 | "_id" : ObjectId("569e5eed6815b47ce1bdb583"), 130 | "document" : "569e5eed6815b47ce7bdb583", 131 | "date": parser.parse("2016-02-01"), 132 | "type": "edit", 133 | "title": "Nutella, Coffee, ninja", 134 | }) 135 | docs.append({ 136 | "_id" : ObjectId("569e5eed6815b47ce1bdb584"), 137 | "document": "569e5eed6815b47ce7bdb583", 138 | "date": parser.parse("2016-01-30"), 139 | "type": "add", 140 | "title": "Nutella, Coffee, ninja", 141 | }) 142 | docs.append({ 143 | "_id" : ObjectId("569e5eed6815b47ce1bdb585"), 144 | "document" : "569e5eed6815b47ce7bdb585", 145 | "date": parser.parse("2016-01-30"), 146 | "type": "show", 147 | "title": "Whoop Whoop Reloaded", 148 | }) 149 | docs.append({ 150 | "_id" : ObjectId("569e5eed6815b47ce1bdb586"), 151 | "document" : ObjectId("569e5eed6815b47ce7bdb586"), 152 | "date": parser.parse("2016-01-30"), 153 | "type": "delete", 154 | "title": "How to get in touch with mocks", 155 | }) 156 | 157 | c = mongomock.MongoClient().db.collection 158 | 159 | for doc in docs: 160 | c.insert(doc) 161 | return c 162 | 163 | def mock_editor(editor, template=""): 164 | template = template + "TEST" 165 | return template, "/tmp/THISNEVERMATCHES" 166 | 167 | def mock_config(): 168 | f = { 169 | 'mailfrom': 'user@example.net', 170 | 'shortids': 'cache', 171 | 'transactions': 'transactions', 172 | 'config': 'config', 173 | 'db': 'rvo', 174 | 'uri': 'mongodb://user:pass@localhost/rvo', 175 | 'collection': 'documents', 176 | 'editor': 'vim', 177 | 'pager': 'vim -R' 178 | } 179 | return f 180 | 181 | def normalize_element_without_regex(filter, field): 182 | """ 183 | mongomock does not support regex... 184 | """ 185 | if len(filter) > 0: 186 | filters = {field: { "$in": filter}} 187 | else: 188 | filters = {} 189 | 190 | print filters 191 | return filters 192 | 193 | # Monkeypatch the shit out of rvo 194 | @pytest.fixture(autouse=True) 195 | def get_document_collection(monkeypatch): 196 | monkeypatch.setattr(rvo.db, "get_document_collection", mock_documents_collection) 197 | 198 | @pytest.fixture(autouse=True) 199 | def get_shortids_collection(monkeypatch): 200 | monkeypatch.setattr(rvo.db, "get_shortids_collection", mock_shortids_collection) 201 | 202 | @pytest.fixture(autouse=True) 203 | def get_config_collection(monkeypatch): 204 | monkeypatch.setattr(rvo.db, "get_config_collection", mock_config_collection) 205 | 206 | @pytest.fixture(autouse=True) 207 | def get_transactions_collection(monkeypatch): 208 | monkeypatch.setattr(rvo.db, "get_transactions_collection", mock_transactions_collection) 209 | 210 | @pytest.fixture(autouse=True) 211 | def get_editor(monkeypatch): 212 | monkeypatch.setattr(rvo.utils, "get_content_from_editor", mock_editor) 213 | 214 | @pytest.fixture(autouse=True) 215 | def get_config(monkeypatch): 216 | monkeypatch.setattr(rvo.config, "parse_config", mock_config) 217 | 218 | @pytest.fixture() 219 | def isatty_false(monkeypatch): 220 | monkeypatch.setattr(rvo.utils, "isatty", False) 221 | 222 | @pytest.fixture() 223 | def isatty_true(monkeypatch): 224 | monkeypatch.setattr(rvo.utils, "isatty", True) 225 | 226 | @pytest.fixture() 227 | def validation_item(): 228 | item = { 229 | "content" : "Content", 230 | "created": datetime.datetime.now(), 231 | "updated": datetime.datetime.now(), 232 | "tags": [ "mongodb", "markdown" ], 233 | "categories": ["notes"], 234 | "encrypted": False, 235 | "title": "My very first entry", 236 | } 237 | return item 238 | 239 | 240 | @pytest.fixture() 241 | def mock_datetime_today(monkeypatch): 242 | monkeypatch.setattr(datetime.datetime, "now", datetime(2016, 4, 11, 17, 6, 14, 121180)) 243 | 244 | @pytest.fixture(autouse=True) 245 | def mock_normalize_element(monkeypatch): 246 | monkeypatch.setattr(rvo.utils, "normalize_element", normalize_element_without_regex) 247 | 248 | # Reusable functions from within tests. 249 | def rvo_output(options, output): 250 | runner = CliRunner() 251 | print options 252 | result = runner.invoke(cli=cli.cli, args=options) 253 | print result 254 | for out in output: 255 | assert out in result.output 256 | assert result.exit_code == 0 257 | assert not result.exception 258 | 259 | def rvo_err(options): 260 | runner = CliRunner() 261 | result = runner.invoke(cli.cli, options) 262 | assert result.exit_code == 1 263 | assert result.exception 264 | 265 | -------------------------------------------------------------------------------- /tests/test_add.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | from conftest import rvo_output, rvo_err 6 | from click.testing import CliRunner 7 | from rvo import cli 8 | 9 | def test_add_all_parameters(isatty_true): 10 | options = ['add', '-t', 'test', '-c', 'test', '--content', 'test'] 11 | output = ['Document "test" created.'] 12 | rvo_output(options,output) 13 | 14 | def test_add_tags(isatty_true): 15 | options = ['add', '-t', 'test', '--content', 'test'] 16 | output = ['Document "test" created.'] 17 | rvo_output(options,output) 18 | 19 | def test_add_title_test(isatty_true): 20 | options = ['add', '-t', 'test', '--content', 'THIS IS A TITLE'] 21 | output = ['Document "THIS IS A TITLE" created.'] 22 | rvo_output(options,output) 23 | 24 | def test_add_title_test_gnarf(isatty_true): 25 | runner = CliRunner() 26 | result = runner.invoke(cli.cli, ['add', '-c', 'töstcät', '-x', 'gnarf']) 27 | assert not result.exception 28 | assert result.output.strip().endswith('Document "gnarf" created.') 29 | 30 | def test_add_title_test_gnarf(isatty_true): 31 | runner = CliRunner() 32 | result = runner.invoke(cli.cli, ['add', '-c', 'töstcät', '-x', 'gnarf\nfoo']) 33 | assert not result.exception 34 | assert result.output.strip().endswith('Document "gnarf" created.') 35 | 36 | def test_add_title_test_hashtag(isatty_true): 37 | options = ['add', '-t', 'test', '--content', '# THIS IS A TITLE'] 38 | output = ['Document "THIS IS A TITLE" created.'] 39 | rvo_output(options,output) 40 | 41 | def test_add_title_test_hashtag(isatty_true): 42 | options = ['add', '-t', 'test', '--content', '# THIS IS A TITLE\nmutliline'] 43 | output = ['Document "THIS IS A TITLE" created.'] 44 | rvo_output(options,output) 45 | 46 | def test_add_very_long_title(isatty_true): 47 | options = ['add', '-t', 'test', '--content', '# THIS IS A VERY VERY LONG NEVER ENDING TITLE THAT EXCEEDS LIMITS'] 48 | output = ['Document "THIS IS A VERY VERY LONG NEVER ENDING TITLE THAT E" created.'] 49 | rvo_output(options,output) 50 | 51 | def test_add_no_parameters(isatty_true): 52 | runner = CliRunner() 53 | result = runner.invoke(cli.cli, ['add']) 54 | assert result.output.strip().endswith('Document "TEST" created.') 55 | assert not result.exception 56 | 57 | def test_add_one_parameters_tag(isatty_true): 58 | runner = CliRunner() 59 | result = runner.invoke(cli.cli, ['add', '-t', 'testtag']) 60 | assert result.output.strip().endswith('Document "TEST" created.') 61 | assert not result.exception 62 | 63 | def test_add_utf8_cat(isatty_true): 64 | runner = CliRunner() 65 | result = runner.invoke(cli.cli, ['add', '-c', 'töstcät']) 66 | assert result.output.strip().endswith('Document "TEST" created.') 67 | assert not result.exception 68 | 69 | def test_add_utf8_cat_multi(isatty_true): 70 | runner = CliRunner() 71 | result = runner.invoke(cli.cli, ['add', '-c', 'tüütüü', '-c', 'töstcät']) 72 | assert result.output.strip().endswith('Document "TEST" created.') 73 | assert not result.exception 74 | 75 | def test_add_utf8_tag(isatty_true): 76 | runner = CliRunner() 77 | result = runner.invoke(cli.cli, ['add', '-t', 'töstcät']) 78 | assert result.output.strip().endswith('Document "TEST" created.') 79 | assert not result.exception 80 | 81 | def test_add_utf8_tag_multi(isatty_true): 82 | runner = CliRunner() 83 | result = runner.invoke(cli.cli, ['add', '-t', 'tüütüü', '-t', 'töstcät']) 84 | assert result.output.strip().endswith('Document "TEST" created.') 85 | assert not result.exception 86 | 87 | def test_add_encrypt_by_parameter_wrong_pw(isatty_true): 88 | runner = CliRunner() 89 | result = runner.invoke(cli.cli, ['add', '-e', '-p', 'thispasswordistotallywrong', '-t', 'encryption', '-c', 'test']) 90 | assert result.output.strip().endswith('Invalid Password') 91 | assert result.exception 92 | 93 | def test_add_encrypt_by_parameter(isatty_true): 94 | runner = CliRunner() 95 | result = runner.invoke(cli.cli, ['add', '-e', '-p', 'test123', '-t', 'encryption', '-c', 'test']) 96 | assert result.output.strip().endswith('Document "TEST" created.') 97 | assert not result.exception 98 | 99 | def test_add_encrypt_by_input(isatty_true): 100 | runner = CliRunner() 101 | result = runner.invoke(cli.cli, ['add', '-e', '-t', 'encryption', '-c', 'test'], input="test123\n") 102 | assert result.output.strip().endswith('Document "TEST" created.') 103 | assert not result.exception 104 | 105 | def test_add_encrypt_by_input_with_content(isatty_true): 106 | runner = CliRunner() 107 | result = runner.invoke(cli.cli, ['add', '-e', '-t', 'encryption', '-x', 'TEST', '-c', 'test'], input="test123\n") 108 | assert result.output.strip().endswith('Document "TEST" created.') 109 | assert not result.exception 110 | 111 | def test_add_encrypt_by_input_wrong_pw(isatty_true): 112 | runner = CliRunner() 113 | result = runner.invoke(cli.cli, ['add', '-e', '-t', 'encryption', '-c', 'test'], input="test2123\n") 114 | assert result.output.strip().endswith('Invalid Password') 115 | assert result.exception 116 | 117 | def test_add_read_from_stdin(isatty_false): 118 | runner = CliRunner() 119 | result = runner.invoke(cli.cli, ['add'], input="Schwifty\nSchwifty..lol\nMorty\n\n") 120 | assert result.output.strip().endswith('Document "Schwifty" created.') 121 | assert not result.exception 122 | 123 | def test_add_read_from_stdin_with_cat(isatty_false): 124 | runner = CliRunner() 125 | result = runner.invoke(cli.cli, ['add', '-c', 'test'], input="Schwifty\nSchwifty..lol\nMorty\n\n") 126 | assert result.output.strip().endswith('Document "Schwifty" created.') 127 | assert not result.exception 128 | 129 | def test_add_read_from_stdin_with_tag(isatty_false): 130 | runner = CliRunner() 131 | result = runner.invoke(cli.cli, ['add', '-t', 'tag'], input="Schwifty\nSchwifty..lol\nMorty\n\n") 132 | assert not result.exception 133 | assert result.output.strip().endswith('Document "Schwifty" created.') 134 | 135 | def test_add_conflicting_stdin_reading(isatty_false): 136 | runner = CliRunner() 137 | result = runner.invoke(cli.cli, ['add', '-e'], input="Schwifty\nSchwifty..lol\nMorty\n\n") 138 | assert result.exception 139 | assert result.output.strip().endswith('Invalid Password') 140 | 141 | def test_add_location_germany(isatty_true): 142 | runner = CliRunner() 143 | result = runner.invoke(cli.cli, ['add', '-l', 'Nuremberg', '-c', 'test']) 144 | assert result.output.strip().endswith('Document "TEST" created.') 145 | assert not result.exception 146 | 147 | def test_add_location_invalid(isatty_true): 148 | runner = CliRunner() 149 | result = runner.invoke(cli.cli, ['add', '-l', 'DOESNOTEXISTTOWNATLEASTIHOPE', '-c', 'test']) 150 | assert result.exception 151 | -------------------------------------------------------------------------------- /tests/test_append.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_append(): 6 | runner = CliRunner() 7 | result = runner.invoke(cli.cli, ['append', '569e5eed6815b47ce7bdb583'], input="foo\n") 8 | assert not result.exception 9 | assert result.output.strip().endswith('Content appended to "Nutella, Coffee, ninja".') 10 | 11 | def test_append_stdin(): 12 | runner = CliRunner() 13 | result = runner.invoke(cli.cli, ['append', '569e5eed6815b47ce7bdb583'], input="foo\n") 14 | assert not result.exception 15 | assert result.output.strip().endswith('Content appended to "Nutella, Coffee, ninja".') 16 | 17 | def test_append_content(): 18 | options = ['append', '569e5eed6815b47ce7bdb583', '-x', 'APPEND'] 19 | output = ['Content appended to "Nutella, Coffee, ninja".'] 20 | rvo_output(options,output) 21 | 22 | def test_append_content_encrypted(): 23 | options = ['append', '569e5eed6815b47ce7bdb587', '-p', 'test123', '-x', 'APPEND'] 24 | output = ['Content appended to "pynacl is just greatAPPEND".'] 25 | rvo_output(options,output) 26 | 27 | def test_append_content_long(): 28 | options = ['append', '569e5eed6815b47ce7bdb583', '--content', 'APPEND'] 29 | output = ['Content appended to "Nutella, Coffee, ninja".'] 30 | rvo_output(options,output) 31 | 32 | def test_append_input_none(): 33 | runner = CliRunner() 34 | result = runner.invoke(cli.cli, ['append', '569e5eed6815b47ce7bdb583'], input="foo\n") 35 | assert not result.exception 36 | assert result.output.strip().endswith('Content appended to "Nutella, Coffee, ninja".') 37 | 38 | def test_append_nonexistent(): 39 | options = ['append', '769e5eed6815b47ce7bdb583'] 40 | rvo_err(options) 41 | 42 | def test_append_shortid_content(): 43 | options = ['append', '-x', 'APPEND', '2'] 44 | output = ['Content appended to "just broke my leg".'] 45 | rvo_output(options,output) 46 | 47 | def test_append_shortid_content_encrypted(): 48 | options = ['append', '-p', 'test123', '-x', 'APPEND', '5'] 49 | output = ['Content appended to "pynacl is just greatAPPEND".'] 50 | rvo_output(options,output) 51 | 52 | def test_append_shortid_content_long(): 53 | options = ['append', '--content', 'APPEND', '2'] 54 | output = ['Content appended to "just broke my leg".'] 55 | rvo_output(options,output) 56 | 57 | def test_append_shortid_stdin(): 58 | runner = CliRunner() 59 | result = runner.invoke(cli.cli, ['append', '1'], input="foo\n") 60 | assert not result.exception 61 | assert result.output.strip().endswith('Content appended to "Nutella, Coffee, ninja".') 62 | 63 | def test_append_shortid_nonexistent(): 64 | options = ['append', '7'] 65 | rvo_err(options) 66 | -------------------------------------------------------------------------------- /tests/test_delete.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_delete_yes(): 6 | options = ['delete', '569e5eed6815b47ce7bdb583', '--yes'] 7 | output = ["Removed"] 8 | rvo_output(options,output) 9 | 10 | def test_delete_input_yes(): 11 | runner = CliRunner() 12 | result = runner.invoke(cli.cli, ['delete', '569e5eed6815b47ce7bdb583'], input="y\n") 13 | assert not result.exception 14 | assert result.output.strip().endswith('Removed Nutella, Coffee, ninja') 15 | 16 | def test_delete_input_no(): 17 | runner = CliRunner() 18 | result = runner.invoke(cli.cli, ['delete', '569e5eed6815b47ce7bdb583'], input="n\n") 19 | assert not result.exception 20 | assert not result.output.strip().endswith('Removed Nutella, Coffee, ninja') 21 | 22 | def test_delete_input_default(): 23 | runner = CliRunner() 24 | result = runner.invoke(cli.cli, ['delete', '569e5eed6815b47ce7bdb583'], input="\n") 25 | assert not result.exception 26 | assert not result.output.strip().endswith('Removed Nutella, Coffee, ninja') 27 | 28 | def test_delete_nonexistent(): 29 | options = ['delete', '769e5eed6815b47ce7bdb583'] 30 | rvo_err(options) 31 | 32 | def test_delete_shortid_yes(): 33 | options = ['delete', '2', '--yes'] 34 | output = ["Removed"] 35 | rvo_output(options,output) 36 | 37 | def test_delete_shortid_input_yes(): 38 | runner = CliRunner() 39 | result = runner.invoke(cli.cli, ['delete', '1'], input="y\n") 40 | assert not result.exception 41 | assert result.output.strip().endswith('Removed Nutella, Coffee, ninja') 42 | 43 | def test_delete_shortid_input_no(): 44 | runner = CliRunner() 45 | result = runner.invoke(cli.cli, ['delete', '1'], input="n\n") 46 | assert not result.exception 47 | assert not result.output.strip().endswith('Removed Nutella, Coffee, ninja') 48 | 49 | def test_delete_shortid_input_default(): 50 | runner = CliRunner() 51 | result = runner.invoke(cli.cli, ['delete', '1'], input="\n") 52 | assert not result.exception 53 | assert not result.output.strip().endswith('Removed Nutella, Coffee, ninja') 54 | 55 | def test_delete_shortid_nonexistent(): 56 | options = ['delete', '7'] 57 | rvo_err(options) 58 | -------------------------------------------------------------------------------- /tests/test_edit.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_edit(): 6 | options = ['edit', '569e5eed6815b47ce7bdb584'] 7 | output = ['Document "just broke my leg" updated.'] 8 | rvo_output(options,output) 9 | 10 | def test_edit_shortid(): 11 | options = ['edit', '2'] 12 | output = ['Document "just broke my leg" updated.'] 13 | rvo_output(options,output) 14 | 15 | def test_edit_encrypt(): 16 | runner = CliRunner() 17 | result = runner.invoke(cli.cli, ['edit', '-p', 'test123', '5']) 18 | assert not result.exception 19 | assert result.output.strip().endswith('Document "pynacl is just greatTEST" updated.') 20 | 21 | def test_edit_encrypt_wrong_pw(): 22 | runner = CliRunner() 23 | result = runner.invoke(cli.cli, ['edit', '-p', 'wrongpw', '5']) 24 | assert result.output.strip().endswith('Invalid Password') 25 | assert result.exception 26 | 27 | def test_edit_encrypt_by_input(): 28 | runner = CliRunner() 29 | result = runner.invoke(cli.cli, ['edit', '5'], input="test123\n") 30 | assert result.output.strip().endswith('Document "pynacl is just greatTEST" updated.') 31 | assert not result.exception 32 | 33 | def test_edit_encrypt_by_input_wrong_pw(): 34 | runner = CliRunner() 35 | result = runner.invoke(cli.cli, ['edit', '5'], input="wrongpw\n") 36 | assert result.output.strip().endswith('Invalid Password') 37 | assert result.exception 38 | -------------------------------------------------------------------------------- /tests/test_export.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_export(): 6 | options = ['export'] 7 | output = ['ninja', 'categories', 'date', 'created', 'updated'] 8 | rvo_output(options,output) 9 | 10 | def test_export_encrypt(): 11 | options = ['export', '-p', 'test123'] 12 | output = ['ninja', 'pynacl', 'date', 'created', 'updated'] 13 | rvo_output(options,output) 14 | 15 | 16 | def test_export_encrypt_wrong_pw(): 17 | runner = CliRunner() 18 | result = runner.invoke(cli.cli, ['export', '-p', 'wrongpw', ]) 19 | assert result.output.strip().endswith('Invalid Password') 20 | assert result.exception 21 | 22 | def test_export_json(): 23 | options = ['export', '--to', 'json'] 24 | output = ['ninja', 'categories', 'date', 'created', 'updated'] 25 | rvo_output(options,output) 26 | 27 | def test_export_encrypt_json(): 28 | options = ['export', '-p', 'test123', '--to', 'json'] 29 | output = ['ninja', 'pynacl', 'date', 'created', 'updated'] 30 | rvo_output(options,output) 31 | 32 | def test_export_markdown(): 33 | options = ['export', '--to', 'markdown'] 34 | output = ['broke', 'my', 'leg' ] 35 | rvo_output(options,output) 36 | 37 | def test_export_encrypt_markdown(): 38 | options = ['export', '-p', 'test123', '--to', 'markdown'] 39 | output = ['broke', 'pynacl', 'leg' ] 40 | rvo_output(options,output) 41 | 42 | def test_export_by_id(): 43 | options = ['export', '--id', '3'] 44 | output = ['Whoop', 'leg', 'again', 'date', 'created', 'updated'] 45 | rvo_output(options,output) 46 | 47 | def test_export_by_category(): 48 | options = ['export', '-c', 'docs'] 49 | output = ['How', 'get', 'date', 'created', 'updated'] 50 | rvo_output(options,output) 51 | 52 | def test_export_by_tag(): 53 | options = ['export', '-t', 'holiday'] 54 | output = ['Whoop', 'leg', 'again', 'date', 'created', 'updated'] 55 | rvo_output(options,output) 56 | 57 | def test_export_by_category_with_password(): 58 | options = ['export', '-c', 'crypto', '-p', 'test123'] 59 | output = ['pynacl', 'date', 'created', 'updated'] 60 | rvo_output(options,output) 61 | 62 | def test_export_by_id_encrypted(): 63 | options = ['export', '--id', '5', '-p', 'test123'] 64 | output = ['pynacl', 'date', 'created', 'updated'] 65 | rvo_output(options,output) 66 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_import(): 6 | options = ['rimport'] 7 | output = [''] 8 | rvo_output(options,output) 9 | -------------------------------------------------------------------------------- /tests/test_info.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | 3 | def test_info(): 4 | options = ['info', '569e5eed6815b47ce7bdb583'] 5 | output = ["list"] 6 | rvo_output(options,output) 7 | 8 | def test_info_err(): 9 | options = ['info', '769e5eed6815b47ce7bdb583'] 10 | rvo_err(options) 11 | 12 | def test_info_shortid(): 13 | options = ['info', '1'] 14 | output = ["list"] 15 | rvo_output(options,output) 16 | 17 | def test_info_shortid_err(): 18 | options = ['info', '7'] 19 | rvo_err(options) 20 | -------------------------------------------------------------------------------- /tests/test_list.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | from click.testing import CliRunner 3 | 4 | def test_list(): 5 | options = ['list'] 6 | output = ['Tags', 'Cats', 'ID', 'Title'] 7 | rvo_output(options,output) 8 | 9 | # def test_list_content(): 10 | # options = ['list', '-x', 'Nutrlla'] 11 | # output = ['Whoop'] 12 | # rvo_output(options,output) 13 | 14 | def test_list_title(): 15 | options = ['list', '-s', 'Nutella'] 16 | output = ['Nutella'] 17 | rvo_output(options,output) 18 | 19 | def test_list_categories(): 20 | options = ['list', '-c', 'notes'] 21 | output = ["notes"] 22 | rvo_output(options,output) 23 | 24 | def test_list_categories_two(): 25 | options = ['list', '-c', 'jrnl'] 26 | output = ["Whoop", "Reloaded"] 27 | rvo_output(options,output) 28 | 29 | def test_list_categories_two(): 30 | options = ['list', '-c', 'jrnl', '-c', 'docs'] 31 | output = ['jrnl', 'docs'] 32 | rvo_output(options,output) 33 | 34 | def test_list_limit_one(): 35 | options = ['list', '-c', 'jrnl', '-c', 'docs', '-l', '1'] 36 | output = ['jrnl', '1 out of 3 result'] 37 | rvo_output(options,output) 38 | 39 | def test_list_limit_overload(): 40 | options = ['list', '-c', 'jrnl', '-c', 'docs', '-l', '300'] 41 | output = ['jrnl', 'docs', '3 out of 3 result'] 42 | rvo_output(options,output) 43 | 44 | def test_list_tags(): 45 | options = ['list', '-t', 'list'] 46 | output = ["list"] 47 | rvo_output(options,output) 48 | 49 | def test_list_tags_two(): 50 | options = ['list', '-t', 'list', '-t', 'todo'] 51 | output = ['todo', 'list'] 52 | rvo_output(options,output) 53 | 54 | def test_list_from_to_year(): 55 | options = ['list', '--from', '2015', '--to', '2016'] 56 | output = ['1 out of 1 result(s)', 'notes'] 57 | rvo_output(options,output) 58 | 59 | def test_list_from_year(): 60 | options = ['list', '--from', '2014'] 61 | output = ['3 out of 3 result(s)', 'jrnl'] 62 | rvo_output(options,output) 63 | 64 | def test_list_to_year(): 65 | options = ['list', '--to', '2014'] 66 | output = ['2 out of 2 result(s)', 'holiday', 'Reloaded'] 67 | rvo_output(options,output) 68 | 69 | def test_list_to_date(): 70 | options = ['list', '--to', '2015-02-03'] 71 | output = ['3 out of 3 result(s)', 'holiday', 'Reloaded'] 72 | rvo_output(options,output) 73 | 74 | def test_list_from_date(): 75 | options = ['list', '--from', '2015-02-03'] 76 | output = ['2 out of 2 result(s)', 'holiday', 'Reloaded'] 77 | rvo_output(options,output) 78 | 79 | def test_list_from_date(): 80 | options = ['list', '--from', '2015-01-30', '--to', '2015-02-03'] 81 | output = ['1 out of 1 result(s)', 'Nutella', 'Coffee'] 82 | rvo_output(options,output) 83 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_log(): 6 | options = ['log'] 7 | output = ["ninja", "Whoop", "Type", "Title", "Nutella", "delete", "show"] 8 | rvo_output(options,output) 9 | 10 | def test_log_limit_two(): 11 | runner = CliRunner() 12 | result = runner.invoke(cli.cli, ['log', '-e', '1']) 13 | assert not result.exception 14 | assert result.output.strip().endswith('569e5eed6815b47ce1bdb583') 15 | 16 | def test_log_limit_one(): 17 | runner = CliRunner() 18 | result = runner.invoke(cli.cli, ['log', '-e', '2']) 19 | assert not result.exception 20 | assert 'edit' in result.output.strip() 21 | assert result.output.strip().endswith("569e5eed6815b47ce1bdb583") 22 | -------------------------------------------------------------------------------- /tests/test_memories.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_memories(): 6 | options = ['memories', '-d', '2017-03-01'] 7 | output = ["Whoop", "jrnl", "holiday", "vacation"] 8 | rvo_output(options,output) 9 | 10 | def test_memories_detail(): 11 | options = ['memories', '-d', '2017-03-01', '-f', 'detail'] 12 | output = ["Whoop", "jrnl", "holiday", "vacation"] 13 | rvo_output(options,output) 14 | 15 | -------------------------------------------------------------------------------- /tests/test_modify.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output, rvo_err 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_modify_all_parameters(): 6 | options = ['modify', '-t', 'foo', '-c', 'test', '569e5eed6815b47ce7bdb583'] 7 | output = ['Updated', 'categories', 'test'] 8 | rvo_output(options,output) 9 | 10 | def test_modify_no_category(): 11 | options = ['modify', '-t', 'footag', '-c', 'None', '569e5eed6815b47ce7bdb583'] 12 | output = ['Updated', 'tags', 'footag'] 13 | rvo_output(options,output) 14 | 15 | def test_modify_no_tag(): 16 | options = ['modify', '-t', 'None', '-c', 'foocat', '569e5eed6815b47ce7bdb583'] 17 | output = ['Updated', 'categories', 'foocat'] 18 | rvo_output(options,output) 19 | 20 | def test_modify_two_cats(): 21 | options = ['modify', '-t', 'foo', '-t', 'bar', '-c' ,'None', '569e5eed6815b47ce7bdb583'] 22 | output = ['Updated', 'foo', 'bar'] 23 | rvo_output(options,output) 24 | 25 | def test_modify_no_cats_no_tags(): 26 | options = ['modify', '569e5eed6815b47ce7bdb583'] 27 | output = [''] 28 | rvo_output(options,output) 29 | 30 | def test_modify_two_cats(): 31 | options = ['modify', '-t', 'None', '-c', 'foocat', '-c' ,'secondcat', '569e5eed6815b47ce7bdb583'] 32 | output = ['Updated', 'categories', 'foocat'] 33 | rvo_output(options,output) 34 | 35 | def test_modify_no_parameters(): 36 | runner = CliRunner() 37 | result = runner.invoke(cli.cli, ['modify', '569e5eed6815b47ce7bdb583']) 38 | assert not result.exception 39 | 40 | def test_modify_no_parameters(): 41 | runner = CliRunner() 42 | result = runner.invoke(cli.cli, ['modify', '569e5eed6815b47ce7bdb585']) 43 | assert not result.exception 44 | 45 | def test_modify_shortid_all_parameters(): 46 | options = ['modify', '-t', 'foo', '-c', 'test', '1'] 47 | output = ['Updated', 'categories', 'test'] 48 | rvo_output(options,output) 49 | 50 | def test_modify_shortid_no_category(): 51 | options = ['modify', '-t', 'footag', '-c', 'None', '1'] 52 | output = ['Updated', 'tags', 'footag'] 53 | rvo_output(options,output) 54 | 55 | def test_modify_shortid_no_tag(): 56 | options = ['modify', '-t', 'None', '-c', 'foocat', '1'] 57 | output = ['Updated', 'categories', 'foocat'] 58 | rvo_output(options,output) 59 | 60 | def test_modify_shortid_two_cats(): 61 | options = ['modify', '-t', 'foo', '-t', 'bar', '-c' ,'None', '1'] 62 | output = ['Updated', 'foo', 'bar'] 63 | rvo_output(options,output) 64 | 65 | def test_modify_shortid_no_cats_no_tags(): 66 | options = ['modify', '1'] 67 | output = [''] 68 | rvo_output(options,output) 69 | 70 | def test_modify_shortid_two_cats(): 71 | options = ['modify', '-t', 'None', '-c', 'foocat', '-c' ,'secondcat', '1'] 72 | output = ['Updated', 'categories', 'foocat'] 73 | rvo_output(options,output) 74 | 75 | def test_modify_shortid_no_parameters(): 76 | runner = CliRunner() 77 | result = runner.invoke(cli.cli, ['modify', '1']) 78 | assert not result.exception 79 | 80 | 81 | def test_modify_encrypt_by_input_wrong_pw(): 82 | runner = CliRunner() 83 | result = runner.invoke(cli.cli, ['modify', '-e', 'yes', '1'], input="wrongpw\n") 84 | assert result.exception 85 | assert result.output.strip().endswith('Invalid Password') 86 | 87 | def test_modify_encrypt_by_input(): 88 | runner = CliRunner() 89 | result = runner.invoke(cli.cli, ['modify', '-e', 'yes', '1'], input="test123\n") 90 | assert not result.exception 91 | assert result.output.strip().endswith('ninja is now stored encrypted') 92 | 93 | def test_modify_no_encrypt_by_input(): 94 | runner = CliRunner() 95 | result = runner.invoke(cli.cli, ['modify', '-e', 'no', '1']) 96 | assert not result.exception 97 | assert result.output.strip().endswith('is already stored in plaintext') 98 | -------------------------------------------------------------------------------- /tests/test_show.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_show(): 6 | options = ['show', '569e5eed6815b47ce7bdb583'] 7 | output = ['ninja'] 8 | rvo_output(options,output) 9 | 10 | def test_show_stdout(): 11 | options = ['show', '--stdout', '569e5eed6815b47ce7bdb583'] 12 | output = ['ninja'] 13 | rvo_output(options,output) 14 | 15 | def test_show_shortid(): 16 | options = ['show', '1'] 17 | output = ['ninja', 'Nutella'] 18 | rvo_output(options,output) 19 | 20 | def test_show_encrypt(): 21 | runner = CliRunner() 22 | result = runner.invoke(cli.cli, ['show', '-p', 'test123', '5']) 23 | assert not result.exception 24 | assert result.output.strip().endswith('pynacl is just great') 25 | 26 | def test_show_encrypt_wrong_pw(): 27 | runner = CliRunner() 28 | result = runner.invoke(cli.cli, ['show', '-p', 'wrongpw', '5']) 29 | assert result.output.strip().endswith('Invalid Password') 30 | assert result.exception 31 | 32 | def test_show_encrypt_by_input(): 33 | runner = CliRunner() 34 | result = runner.invoke(cli.cli, ['show', '5'], input="test123\n") 35 | assert result.output.strip().endswith('pynacl is just great') 36 | assert not result.exception 37 | 38 | def test_show_encrypt_by_input_wrong_pw(): 39 | runner = CliRunner() 40 | result = runner.invoke(cli.cli, ['show', '5'], input="wrongpw\n") 41 | assert result.output.strip().endswith('Invalid Password') 42 | assert result.exception 43 | -------------------------------------------------------------------------------- /tests/test_stats.py: -------------------------------------------------------------------------------- 1 | from conftest import rvo_output 2 | from click.testing import CliRunner 3 | from rvo import cli 4 | 5 | def test_stats_encrypt_wrong_pw(): 6 | runner = CliRunner() 7 | result = runner.invoke(cli.cli, ['stats', '-p', 'wrongpw', '-c', 'crypto']) 8 | assert result.output.strip().endswith('Invalid Password') 9 | assert result.exception 10 | 11 | def test_stats_encrypt_tag(): 12 | options = ['stats', '-p', 'test123', '-t', 'crypto'] 13 | output = ['pynacl', 'great'] 14 | rvo_output(options,output) 15 | 16 | def test_stats_by_category_with_password(): 17 | options = ['stats', '-c', 'crypto', '-p', 'test123'] 18 | output = ['pynacl'] 19 | rvo_output(options,output) 20 | 21 | def test_stats_by_id_encrypted(): 22 | options = ['stats', '--id', '5', '-p', 'test123'] 23 | output = ['pynacl' ] 24 | rvo_output(options,output) 25 | -------------------------------------------------------------------------------- /tests/test_validate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from rvo.validate import validate as validate 5 | import datetime 6 | 7 | 8 | def test_validate_pass(validation_item): 9 | assert validate(validation_item) 10 | 11 | def test_validate_wrong_field(validation_item): 12 | del validation_item["content"] 13 | validation_item["contentXXX"] = "FOO" 14 | assert not validate(validation_item) 15 | 16 | def test_validate_missing_field(validation_item): 17 | del validation_item["created"] 18 | assert not validate(validation_item) 19 | 20 | def test_validate_missing_field_title(validation_item): 21 | del validation_item["title"] 22 | assert not validate(validation_item) 23 | 24 | def test_validate_tags_is_not_list_int(validation_item): 25 | validation_item["tags"] = 12 26 | assert not validate(validation_item) 27 | 28 | def test_validate_tags_is_not_list_str(validation_item): 29 | validation_item["tags"] = "string" 30 | assert not validate(validation_item) 31 | 32 | def test_validate_categories_is_not_list_int(validation_item): 33 | validation_item["categories"] = 12 34 | assert not validate(validation_item) 35 | 36 | def test_validate_categories_is_not_list_str(validation_item): 37 | validation_item["categories"] = "string" 38 | assert not validate(validation_item) 39 | 40 | def test_validate_content_unicode(validation_item): 41 | validation_item["content"] = 'ÜÜÜÜÄÄÄÜÜ' 42 | assert validate(validation_item) 43 | 44 | def test_validate_title_unicode(validation_item): 45 | validation_item["title"] = 'ÜÜÜÜÄÄÄÜÜ' 46 | assert validate(validation_item) 47 | 48 | def test_validate_title_list(validation_item): 49 | validation_item["title"] = ["list"] 50 | assert not validate(validation_item) 51 | 52 | def test_validate_content_list(validation_item): 53 | validation_item["content"] = ["list"] 54 | assert not validate(validation_item) 55 | 56 | def test_validate_creatd_as_string(validation_item): 57 | validation_item["created"] = "2016-01-02" 58 | assert not validate(validation_item) 59 | 60 | def test_validate_updated_as_string(validation_item): 61 | validation_item["updated"] = "2016-01-02" 62 | assert not validate(validation_item) 63 | --------------------------------------------------------------------------------