├── .gitignore ├── CHANGELOG.md ├── Extension - CHANGELOG.md ├── LICENSE.md ├── Python-Server └── app │ ├── .dockerignore │ ├── Dockerfile │ ├── background_worker │ ├── background_worker.py │ └── task │ │ ├── remove_task.py │ │ ├── task.py │ │ └── task_manager.py │ ├── docker-compose.yml │ ├── image_tagging │ ├── clarifai_tagging │ │ └── clarifai_tagging.py │ ├── image_tagging.py │ └── tensorflow_tagging │ │ └── tensorflow_tagging.py │ ├── notion │ ├── __init__.py │ ├── block.py │ ├── client.py │ ├── collection.py │ ├── logger.py │ ├── maps.py │ ├── markdown.py │ ├── monitor.py │ ├── operations.py │ ├── records.py │ ├── settings.py │ ├── smoke_test.py │ ├── space.py │ ├── store.py │ ├── user.py │ └── utils.py │ ├── notion_ai │ ├── custom_errors.py │ ├── mind_structure.py │ ├── notion_ai.py │ ├── property_manager │ │ ├── multi_tag_manager.py │ │ ├── property_manager.py │ │ └── tag_object.py │ └── utils.py │ ├── requirements.txt │ ├── server.py │ ├── server_utils │ ├── check_update.py │ ├── handle_options_data.py │ └── utils.py │ ├── static │ ├── error.html │ ├── icon.png │ ├── thank_you.html │ ├── translations │ │ ├── en_US.json │ │ └── es_ES.json │ └── version.cfg │ ├── templates │ └── options.html │ └── translation │ └── translation_manager.py ├── README.md ├── doc ├── add_image.gif ├── add_text.gif ├── add_website.gif ├── clarifai.png ├── collections-example-notion-page.png ├── collections-example.png ├── customize_properties_name.png ├── extension_howto.png ├── get_structure_url.png ├── getting_cookie.png ├── header_collections_example.gif ├── header_collections_example_modify_title_tags.gif ├── header_gif_joined_updated.gif ├── header_gif_joined_updated_collections.gif ├── header_gif_search.gif ├── header_phone_demo.gif ├── notion-database-howto.png ├── options_python.png ├── qr_code_example.png ├── refresh-collections-menu.png ├── server_url_example.png └── settings_howto.png ├── flutter-android-ios-app └── notion_ai_my_mind │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── elblogbruno │ │ │ │ │ └── notion_ai_my_mind │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ └── styles.xml │ │ │ │ └── xml │ │ │ │ └── network_security_config.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── settings_aar.gradle │ ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── pubspec.lock │ ├── pubspec.yaml │ └── test │ └── widget_test.dart ├── icon.png ├── readme-translations └── README.es.md └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/images,video 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=images,video 3 | 4 | /Chrome and Firefox Extension 5 | *.jks 6 | key.properties 7 | /Python-Server/output 8 | /Python-Server/.idea 9 | /Python-Server/app/image_tagging/temp_image_folder 10 | /Python-Server/app/__pycache__ 11 | /Python-Server/app/uploads 12 | 13 | 14 | *.zip 15 | *.log 16 | *.json 17 | !Python-Server/app/static/translations/*.json 18 | !Chrome and Firefox Extension/*.json 19 | ### Images ### 20 | # JPEG 21 | *.jpg 22 | *.jpeg 23 | *.jpe 24 | *.jif 25 | *.jfif 26 | *.jfi 27 | 28 | # JPEG 2000 29 | *.jp2 30 | *.j2k 31 | *.jpf 32 | *.jpx 33 | *.jpm 34 | *.mj2 35 | 36 | # JPEG XR 37 | *.jxr 38 | *.hdp 39 | *.wdp 40 | 41 | 42 | 43 | # RAW 44 | *.raw 45 | 46 | # Web P 47 | *.webp 48 | 49 | 50 | # Animated Portable Network Graphics 51 | *.apng 52 | 53 | # Multiple-image Network Graphics 54 | *.mng 55 | 56 | # Tagged Image File Format 57 | *.tiff 58 | *.tif 59 | 60 | # Scalable Vector Graphics 61 | *.svg 62 | *.svgz 63 | 64 | # Portable Document Format 65 | *.pdf 66 | 67 | # X BitMap 68 | *.xbm 69 | 70 | # BMP 71 | *.bmp 72 | *.dib 73 | 74 | # ICO 75 | *.ico 76 | 77 | # 3D Images 78 | *.3dm 79 | *.max 80 | 81 | ### Video ### 82 | *.3g2 83 | *.3gp 84 | *.asf 85 | *.asx 86 | *.avi 87 | *.flv 88 | *.mov 89 | *.mp4 90 | *.mpg 91 | *.rm 92 | *.swf 93 | *.vob 94 | *.wmv 95 | *.webm 96 | 97 | # End of https://www.toptal.com/developers/gitignore/api/images,video 98 | 99 | # Created by https://www.toptal.com/developers/gitignore/api/python 100 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 101 | 102 | ### Python ### 103 | # Byte-compiled / optimized / DLL files 104 | __pycache__/ 105 | *.py[cod] 106 | *$py.class 107 | 108 | # C extensions 109 | *.so 110 | 111 | # Distribution / packaging 112 | .Python 113 | build/ 114 | develop-eggs/ 115 | dist/ 116 | downloads/ 117 | eggs/ 118 | .eggs/ 119 | lib/ 120 | lib64/ 121 | parts/ 122 | sdist/ 123 | var/ 124 | wheels/ 125 | pip-wheel-metadata/ 126 | share/python-wheels/ 127 | *.egg-info/ 128 | .installed.cfg 129 | *.egg 130 | MANIFEST 131 | 132 | # PyInstaller 133 | # Usually these files are written by a python script from a template 134 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 135 | *.manifest 136 | *.spec 137 | 138 | # Installer logs 139 | pip-log.txt 140 | pip-delete-this-directory.txt 141 | 142 | # Unit test / coverage reports 143 | htmlcov/ 144 | .tox/ 145 | .nox/ 146 | .coverage 147 | .coverage.* 148 | .cache 149 | nosetests.xml 150 | coverage.xml 151 | *.cover 152 | *.py,cover 153 | .hypothesis/ 154 | .pytest_cache/ 155 | pytestdebug.log 156 | 157 | # Translations 158 | *.mo 159 | *.pot 160 | 161 | # Django stuff: 162 | *.log 163 | local_settings.py 164 | db.sqlite3 165 | db.sqlite3-journal 166 | 167 | # Flask stuff: 168 | instance/ 169 | .webassets-cache 170 | 171 | # Scrapy stuff: 172 | .scrapy 173 | 174 | # Sphinx documentation 175 | docs/_build/ 176 | doc/_build/ 177 | 178 | # PyBuilder 179 | target/ 180 | 181 | # Jupyter Notebook 182 | .ipynb_checkpoints 183 | 184 | # IPython 185 | profile_default/ 186 | ipython_config.py 187 | 188 | # pyenv 189 | .python-version 190 | 191 | # pipenv 192 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 193 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 194 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 195 | # install all needed dependencies. 196 | #Pipfile.lock 197 | 198 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 199 | __pypackages__/ 200 | 201 | # Celery stuff 202 | celerybeat-schedule 203 | celerybeat.pid 204 | 205 | # SageMath parsed files 206 | *.sage.py 207 | 208 | # Environments 209 | .env 210 | .venv 211 | env/ 212 | venv/ 213 | ENV/ 214 | env.bak/ 215 | venv.bak/ 216 | pythonenv* 217 | 218 | # Spyder project settings 219 | .spyderproject 220 | .spyproject 221 | 222 | # Rope project settings 223 | .ropeproject 224 | 225 | # mkdocs documentation 226 | /site 227 | 228 | # mypy 229 | .mypy_cache/ 230 | .dmypy.json 231 | dmypy.json 232 | 233 | # Pyre type checker 234 | .pyre/ 235 | 236 | # pytype static type analyzer 237 | .pytype/ 238 | 239 | # profiling data 240 | .prof 241 | 242 | # End of https://www.toptal.com/developers/gitignore/api/python 243 | 244 | 245 | 246 | # Created by https://www.toptal.com/developers/gitignore/api/pycharm 247 | # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm 248 | 249 | ### PyCharm ### 250 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 251 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 252 | 253 | # User-specific stuff 254 | .idea/**/workspace.xml 255 | .idea/**/tasks.xml 256 | .idea/**/usage.statistics.xml 257 | .idea/**/dictionaries 258 | .idea/**/shelf 259 | 260 | # Generated files 261 | .idea/**/contentModel.xml 262 | 263 | # Sensitive or high-churn files 264 | .idea/**/dataSources/ 265 | .idea/**/dataSources.ids 266 | .idea/**/dataSources.local.xml 267 | .idea/**/sqlDataSources.xml 268 | .idea/**/dynamic.xml 269 | .idea/**/uiDesigner.xml 270 | .idea/**/dbnavigator.xml 271 | 272 | # Gradle 273 | .idea/**/gradle.xml 274 | .idea/**/libraries 275 | 276 | # Gradle and Maven with auto-import 277 | # When using Gradle or Maven with auto-import, you should exclude module files, 278 | # since they will be recreated, and may cause churn. Uncomment if using 279 | # auto-import. 280 | # .idea/artifacts 281 | # .idea/compiler.xml 282 | # .idea/jarRepositories.xml 283 | # .idea/modules.xml 284 | # .idea/*.iml 285 | # .idea/modules 286 | # *.iml 287 | # *.ipr 288 | 289 | # CMake 290 | cmake-build-*/ 291 | 292 | # Mongo Explorer plugin 293 | .idea/**/mongoSettings.xml 294 | 295 | # File-based project format 296 | *.iws 297 | 298 | # IntelliJ 299 | out/ 300 | 301 | # mpeltonen/sbt-idea plugin 302 | .idea_modules/ 303 | 304 | # JIRA plugin 305 | atlassian-ide-plugin.xml 306 | 307 | # Cursive Clojure plugin 308 | .idea/replstate.xml 309 | 310 | # Crashlytics plugin (for Android Studio and IntelliJ) 311 | com_crashlytics_export_strings.xml 312 | crashlytics.properties 313 | crashlytics-build.properties 314 | fabric.properties 315 | 316 | # Editor-based Rest Client 317 | .idea/httpRequests 318 | 319 | # Android studio 3.1+ serialized cache file 320 | .idea/caches/build_file_checksums.ser 321 | 322 | ### PyCharm Patch ### 323 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 324 | 325 | # *.iml 326 | # modules.xml 327 | # .idea/misc.xml 328 | # *.ipr 329 | 330 | # Sonarlint plugin 331 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 332 | .idea/**/sonarlint/ 333 | 334 | # SonarQube Plugin 335 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 336 | .idea/**/sonarIssues.xml 337 | 338 | # Markdown Navigator plugin 339 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 340 | .idea/**/markdown-navigator.xml 341 | .idea/**/markdown-navigator-enh.xml 342 | .idea/**/markdown-navigator/ 343 | 344 | # Cache file creation bug 345 | # See https://youtrack.jetbrains.com/issue/JBR-2257 346 | .idea/$CACHE_FILE$ 347 | 348 | # CodeStream plugin 349 | # https://plugins.jetbrains.com/plugin/12206-codestream 350 | .idea/codestream.xml 351 | 352 | # End of https://www.toptal.com/developers/gitignore/api/pycharm 353 | 354 | 355 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode 356 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode 357 | 358 | ### VisualStudioCode ### 359 | .vscode/* 360 | !.vscode/tasks.json 361 | !.vscode/launch.json 362 | *.code-workspace 363 | 364 | ### VisualStudioCode Patch ### 365 | # Ignore all local history of files 366 | .history 367 | .ionide 368 | 369 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.0.6] 9 | 10 | ### Added 11 | - Now you can add a reminder to the content, so you get whatever you added reminded on the future whenever you want. If you select the Autodestroy option, booom , content will be auto-deleted from your mind at that specified time (Wish you had that in real life right?) pd: you can restore deleted notion content from the trash bin :wink:. 12 | - Added a background worker that maintains the database, cleaning blank content added that fills the mind with garbage. (Check for duplicated content coming soon) 13 | - Android app redesign as well, with bug fixes and less immersive experience. 14 | 15 | ### Fixed 16 | - More error handling and structure fix to make everything faster and reliable. 17 | 18 | ## [2.0.5] 19 | 20 | ### Added 21 | 22 | - Now you can add Video files from web or phone! 23 | - Started the base for adding a background worker that maintains the database, cleaning duplicated or blank content added. (Coming soon) 24 | - Tags added from the AI tagging system have a number filter, so they are not all sent at the same time only the 10 first that are more used! 25 | 26 | ### Fixed 27 | - More error handling and structure fix to make everything faster. 28 | - Bug fix https://github.com/elblogbruno/NotionAI-MyMind/issues/20 29 | 30 | ## [2.0.4] 31 | 32 | ### Added 33 | 34 | - Now you can add Audio files from web or phone! 35 | - Added multilanguage-support! Now server, phone app and extension is translated to English and Spanish! Would you like to have it translated into your own language? You can have more info on how to collaborate it helps out people from all comunities and languages access this amazing tool! More info [here](https://github.com/elblogbruno/NotionAI-MyMind-Translations). 36 | 37 | - Chrome extension notifies you when there is a new update on the server! 38 | - The server tells you now if there is a newer version of it on startup! 39 | - Started the base for adding the ability to add notion reminders to content so it is reminded to you in the future. (Coming soon) 40 | - Tags added from the AI tagging system are also suggested to you now when adding tags! 41 | 42 | ### Fixed 43 | - Bug when first image was invalid, it did not add the next ones on the list. 44 | - Bug that when adding content from phone, mind_extension property tell browser-extension instead of phone-extension 45 | - More error handling 46 | - Bug fix https://github.com/elblogbruno/NotionAI-MyMind/discussions/17 47 | ## [2.0.3] 48 | 49 | ### Breaking changes 50 | - Now you can tell on the server the name of the properties, in case you want to customize the name of the basic properties that Notion AI My Mind contacts. 51 | 52 | ### Added 53 | 54 | - Now you can modify the title and url of the added content on the fly from the mind Extensions. 55 | - You can add and remove multiple tags on the content you add. The tags you write will be saved to notion, so they will be suggested to you next time as you write. 56 | It also gets the current tags you may have added in notion for that property, and suggests them to you as you write! 57 | - All this implements the ideas given by the community on #13 and #8. 58 | 59 | - Added ability to scan QR Code on the android app, so you configure the app in no-time. When you access the server, you can scan it with the phone app. 60 | - Fixed bugs and revamped the phone app UI. 61 | - Added more error handling on the phone app. 62 | ### Fixed 63 | 64 | - More error handling 65 | 66 | ## [2.0.2] 67 | 68 | ### Breaking changes 69 | - Notion API has changed. Maybe you get this error: "400 error on get_block() call",the fix is in here https://github.com/jamalex/notion-py/issues/292 but you need to modify manually the library. Meanwhile I am waiting for the library owner to update it. 70 | - If you make a lot of request to notion API, means adding lot of content in a short amount of time, you may get now a 429 error(Too Many Requests). I may look into finding a solution or workaround to this, but it is a problem on notion's not my repo or @jamalex notion-py repo. 71 | 72 | ### Added 73 | 74 | - Added collections! Now you can have different collections or databases, so you can add the content to the collection you choose. More info here as you need to change your current notion setup: https://github.com/elblogbruno/NotionAI-MyMind/wiki/Notion-AI-My-Mind-Collections 75 | 76 | -Android app has updated to allow the new collections! 77 | 78 | ### Fixed 79 | 80 | - Added more exceptions for error handling. 81 | - Added notification for Notion's recent API change that limits api requests. 82 | 83 | ## [2.0.1] 84 | 85 | ### Added 86 | 87 | - Added function to modify a row title and url by its id, this is a base for developing ideas on #8. 88 | - Now it looks at all images available on the content added so we can get more information and AI tag every image. 89 | - Click to open added content works for url's, texts and images now. 90 | - When you add content, it tracks if it was added from your phone or desktop. You need to add a new text atribute called "mind_extension". You can use this to filter by elements added from phone or desktop. 91 | ### Fixed 92 | 93 | - Fixed error #9 94 | - Fixed error no output provided when url or title was none adding a url. 95 | - Fixed error when on some websites where tagged as image not found when images were found. 96 | - Moved utils function to a util file. 97 | 98 | ## [2.0] 99 | 100 | ### Added 101 | 102 | - Added server as a .exe program on windows so it is easier as ever to run Notion AI My Mind in simple clicks. 103 | - Server url is opened automatically on the browser, so you don't get lost if you don't know what the server url is. 104 | 105 | 106 | ### Fixed 107 | 108 | - No port was returned at first run of the server. 109 | - Refactored python directory, organized with folders. 110 | 111 | ## [1.9] 112 | 113 | ### Added 114 | 115 | - Faster and more reliable thanks to switching to an async based server with Quart. 116 | - Added json responses on server with built-in responses that would be translated to other languages. When requesting to the API, it returns the same text and status code on chrome, firefox and android/ios platforms. 117 | - Updated app to work with these changes. 118 | - When clicking on the message box, you can go to the url of the added content and see it. 119 | 120 | ### Fixed 121 | 122 | - Refactored code, made it easier for example to implement in the near future a way to translate to different languages, and removed unused code. 123 | - No message was shown when trying to add something witouth having entered credentials. 124 | 125 | ## [1.8] 126 | 127 | ### Added 128 | 129 | - Added initial Android and Ios App based on flutter. You can download from Play Store here: https://play.google.com/store/apps/details?id=com.elblogbruno.notion_ai_my_mind 130 | - Disclaimer : I won't be releasing the app on the Apple App Store, as I don't have an Apple Developer Account either Mac OS based computer. 131 | 132 | ### Fixed 133 | 134 | - Fixed server issues to be able to add images by posting, so I could add local images from phone. 135 | 136 | ## [1.7] 137 | 138 | ### Added 139 | 140 | - You can log in with email and password. (You need to enable a password on notion website settings.) For security, each time you reload the server you need to log in again. This is because email and password is not saved anywhere. You hear correctly. 141 | - New revamped design and logic of the extension. Now its nicer, and shows always, even adding frome context menu. If you liked it, be careful with this update, you'll love it. 142 | 143 | ### Fixed 144 | 145 | - Fixed server issues adding callback capabilities. Before some websites images were not added due to timeouts. 146 | 147 | [2.0.6]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/2.0.6 148 | [2.0.5]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/2.0.5 149 | [2.0.4]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/2.0.4 150 | [2.0.3]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/2.0.3 151 | [2.0.2]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/2.0.2 152 | [2.0.1]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/2.0.1 153 | [2.0]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/2.0 154 | [1.9]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/1.9 155 | [1.8]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/1.8 156 | [1.7]: https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/1.7 157 | -------------------------------------------------------------------------------- /Extension - CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.1.3] 9 | 10 | ### Fixed 11 | 12 | - Removed unused code. 13 | - Fixed bugs. 14 | ## [1.1.2] 15 | 16 | ### Fixed 17 | 18 | - Removed unused code. 19 | - Fixed bugs. 20 | 21 | ## [1.1.1] 22 | 23 | ### Added 24 | 25 | - It notifies you when there is an update on the server. 26 | - Added option to reload the collections from the menu and close the menu. 27 | - Added Spanish language. More info on how to collaborate in translating [here](https://github.com/elblogbruno/NotionAI-MyMind-Translations) 28 | - Added tag colors as on phone App. 29 | ### Fixed 30 | 31 | - Removed unused code. 32 | - Fixed bugs. 33 | 34 | ## [1.1.0] 35 | 36 | ### Added 37 | 38 | - Added option to modify the title and url of the added content on the fly! 39 | - Added option to add and removes tags to a block. It also gets the current tags from notion, and suggests them to you as you write! 40 | - When you save the settings now, it redirects you to the server and autocompletes the tokenv2, this tries to make it easier for new people to join. 41 | - Revamped the design of the collections popup. 42 | 43 | ### Fixed 44 | 45 | - Removed unused code. 46 | - Fixed bugs. 47 | - Now it shows a loader when waiting that gives more feedback. 48 | 49 | ## [1.0.9] 50 | 51 | ### Added 52 | 53 | - Added collections! Now you can have different collections or databases, so you can add the content to the collection you choose. More info here as you need to change your current notion setup: https://github.com/elblogbruno/NotionAI-MyMind/wiki/Notion-AI-My-Mind-Collections 54 | 55 | 56 | ### Fixed 57 | 58 | - Removed unused code and permissions. 59 | - Now it checks if you enter an incorrect server url. 60 | 61 | ## [1.0.8] 62 | 63 | ### Added 64 | 65 | - Welcome landing page when extension is installed 66 | - Changelog page opens when extension is updated 67 | 68 | ### Fixed 69 | 70 | - Fixed a bug when adding an image showed and alert with the url. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bruno Moya Ruiz 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. -------------------------------------------------------------------------------- /Python-Server/app/.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | **/.classpath 3 | **/.dockerignore 4 | **/.env 5 | **/.git 6 | **/.gitignore 7 | **/.project 8 | **/.settings 9 | **/settings 10 | **/.toolstarget 11 | **/.vs 12 | **/.vscode 13 | **/*.*proj.user 14 | **/*.dbmdl 15 | **/*.jfm 16 | **/azds.yaml 17 | **/bin 18 | **/output 19 | **/charts 20 | **/docker-compose* 21 | **/Dockerfile* 22 | **/node_modules 23 | **/npm-debug.log 24 | **/obj 25 | **/secrets.dev.yaml 26 | **/values.dev.yaml 27 | venv/ 28 | README.md 29 | -------------------------------------------------------------------------------- /Python-Server/app/Dockerfile: -------------------------------------------------------------------------------- 1 | # For more information, please refer to https://aka.ms/vscode-docker-python 2 | FROM python:3.8-slim-buster 3 | 4 | # Keeps Python from generating .pyc files in the container 5 | ENV PYTHONDONTWRITEBYTECODE=1 6 | 7 | # Turns off buffering for easier container logging 8 | ENV PYTHONUNBUFFERED=1 9 | 10 | # Install pip requirements 11 | COPY requirements.txt . 12 | RUN apt-get update 13 | RUN apt-get install git -y 14 | RUN python -m pip install -r requirements.txt 15 | 16 | WORKDIR /app 17 | COPY . /app 18 | 19 | CMD python server.py -------------------------------------------------------------------------------- /Python-Server/app/background_worker/background_worker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | from urllib.error import HTTPError 4 | 5 | import schedule 6 | import time 7 | import threading 8 | import sys 9 | import os 10 | import tempfile 11 | 12 | from background_worker.task.task_manager import TaskManager 13 | 14 | dir_path = 'background_worker.log' 15 | if getattr(sys, 'frozen', False): 16 | tmp = tempfile.mkdtemp() 17 | dir_path = os.path.join(tmp, 'notion-ai-app.log') 18 | 19 | logging.basicConfig(filename=dir_path, filemode='w', format='%(name)s - %(levelname)s - %(message)s', 20 | level=logging.INFO) 21 | 22 | # # define a Handler which writes INFO messages or higher to the sys.stderr 23 | # console = logging.StreamHandler() 24 | # console.setLevel(logging.INFO) 25 | # # add the handler to the root logger 26 | # logging.getLogger('').addHandler(console) 27 | logging.info("Log file will be saved to temporary path: {0}".format(dir_path)) 28 | 29 | class Worker: 30 | def __init__(self, client, notion_ai): 31 | self.client = client 32 | self.notion_ai = notion_ai 33 | self.task_manager = TaskManager(logging, notion_ai) 34 | self.logging = logging 35 | # self.background_job() 36 | schedule.every(5).minutes.do(self.background_job) 37 | 38 | # Start the background thread 39 | self.stop_run_continuously = self.run_continuously() 40 | # 41 | # Do some other things... 42 | # time.sleep(10) 43 | # 44 | # Stop the background thread 45 | # stop_run_continuously.set() 46 | 47 | def run_continuously(self, interval=1): 48 | """Continuously run, while executing pending jobs at each 49 | elapsed time interval. 50 | @return cease_continuous_run: threading. Event which can 51 | be set to cease continuous run. Please note that it is 52 | *intended behavior that run_continuously() does not run 53 | missed jobs*. For example, if you've registered a job that 54 | should run every minute and you set a continuous run 55 | interval of one hour then your job won't be run 60 times 56 | at each interval but only once. 57 | """ 58 | cease_continuous_run = threading.Event() 59 | 60 | class ScheduleThread(threading.Thread): 61 | @classmethod 62 | def run(cls): 63 | while not cease_continuous_run.is_set(): 64 | schedule.run_pending() 65 | time.sleep(interval) 66 | 67 | continuous_thread = ScheduleThread() 68 | continuous_thread.start() 69 | return cease_continuous_run 70 | 71 | def myFunc(self, e): 72 | return e.title == "" 73 | 74 | def _remove_blank_rows(self): 75 | number_of_collections = self.notion_ai.mind_structure.get_number_of_collections() 76 | for i in range(0, number_of_collections): 77 | collection_id, id = self.notion_ai.mind_structure.get_collection_by_index( 78 | i) # collection is our database or "mind" in notion, as be have multiple, if not suplied, it will get the first one as the priority one. 79 | collection = self.client.get_collection(collection_id=collection_id) 80 | 81 | cv = collection.parent.views[0] 82 | self.logging.info("Analysing Collection #{0}".format(i)) 83 | # Run a "filtered" query (inspect network tab in browser for examples, on queryCollection calls) 84 | filter_params = { 85 | "filters": [{ 86 | "filter": { 87 | "operator": "is_empty" 88 | }, 89 | "property": "title" 90 | }], 91 | "operator": "and" 92 | } 93 | result = cv.build_query(filter=filter_params).execute() 94 | 95 | self.logging.info("Analyzing this elements #{0}".format(result)) 96 | 97 | self._remove_blank_blocks_from_list(result) 98 | 99 | self.logging.info("Background work remove blank rows finished at {0}".format(datetime.now())) 100 | 101 | def _remove_blank_blocks_from_list(self, list): 102 | for block in list: 103 | bl = self.client.get_block(block.id) 104 | title = bl.title 105 | url = bl.url 106 | multi_tag = self.notion_ai.property_manager.get_properties(bl, multi_tag_property=1) 107 | ai_tags = self.notion_ai.property_manager.get_properties(bl, ai_tags_property=1) 108 | mind_extension = self.notion_ai.property_manager.get_properties(bl, mind_extension_property=1) 109 | 110 | if len(title) == 0 and len(url) == 0 and len(multi_tag) == 0 and len(ai_tags) == 0 and len( 111 | mind_extension) == 0: 112 | self.logging.info("Removing block with id: {0} as it is blank , title: {1}".format(bl.id, bl.title)) 113 | self.logging.info("Have a look at this if you feel something bad was deleted") 114 | bl.remove() 115 | 116 | def _do_assigned_temporal_tasks(self): 117 | now = datetime.now() 118 | for task in self.task_manager.tasks: 119 | minutes_diff = (task.datetime_to_run - now).total_seconds() / 60.0 120 | if 1 < minutes_diff < 5: 121 | print("Doing temporal tasks at {0}".format(now)) 122 | task.run_task() 123 | self.task_manager.remove_task(task) 124 | 125 | def background_job(self): 126 | if len(self.task_manager.tasks) > 0: 127 | self._do_assigned_temporal_tasks() 128 | 129 | time.sleep(2) 130 | 131 | try: 132 | self._remove_blank_rows() 133 | except HTTPError as e: 134 | logging.info(str(e)) 135 | 136 | -------------------------------------------------------------------------------- /Python-Server/app/background_worker/task/remove_task.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from background_worker.task.task import Task 4 | 5 | 6 | class RemoveTask(Task): 7 | def __init__(self, block_id_to_remove= None, datetime_to_run= None, client= None, dic = None): 8 | if client: 9 | self.notion_client = client 10 | if dic: 11 | super().__init__(type="RemoveTask", dic = dic) 12 | elif block_id_to_remove and datetime_to_run: 13 | super().__init__(type="RemoveTask", task_id=block_id_to_remove, datetime_to_run=datetime_to_run) 14 | self.block_id_to_remove = block_id_to_remove 15 | 16 | 17 | def __str__(self): 18 | print("Task of type {0} at this time: {1}".format(self.type, self.datetime_to_run)) 19 | 20 | def run_task(self): 21 | print("Removing block with id {0}".format(self.block_id_to_remove)) 22 | block = self.notion_client.get_block(self.block_id_to_remove) 23 | block.remove() 24 | print("Block removed succesfully!") 25 | 26 | def _from_dic(self, dic): 27 | self.type = dic['type'] 28 | self.task_id = dic['task_id'] 29 | self.datetime_to_run = datetime.strptime(dic['datetime_to_run'], "%Y-%m-%d %H:%M:%S") 30 | self.block_id_to_remove = dic['block_id_to_remove'] 31 | 32 | def to_dic(self): 33 | dic = { 34 | "type": self.type, 35 | "task_id": self.task_id, 36 | "block_id_to_remove": self.block_id_to_remove, 37 | "datetime_to_run": str(self.datetime_to_run) 38 | } 39 | self.task_dictionary = dic 40 | return self.task_dictionary 41 | -------------------------------------------------------------------------------- /Python-Server/app/background_worker/task/task.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class Task: 5 | 6 | def __init__(self, type="Task", task_id = None, datetime_to_run= None, dic= None): 7 | if dic: 8 | self._from_dic(dic) 9 | elif task_id and datetime_to_run and type: 10 | self.type = type 11 | self.task_id = task_id 12 | self.datetime_to_run = datetime_to_run 13 | self.task_dictionary = {} 14 | 15 | def _from_dic(self, dic): 16 | self.type = dic['type'] 17 | self.task_id = dic['task_id'] 18 | self.datetime_to_run = datetime.strptime(dic['datetime_to_run'], "%Y-%m-%d %H:%M") 19 | 20 | def to_dic(self): 21 | dic = { 22 | "type": self.type, 23 | "task_id": self.task_id, 24 | "datetime_to_run": str(self.datetime_to_run) 25 | } 26 | self.task_dictionary = dic 27 | return self.task_dictionary 28 | 29 | def __str__(self): 30 | print("Task of type {0} at this time: {1}".format(self.type, self.datetime_to_run)) 31 | 32 | def run_task(self): 33 | print("Running task of type {0} at this time: {1}".format(self.type, self.datetime_to_run)) 34 | 35 | -------------------------------------------------------------------------------- /Python-Server/app/background_worker/task/task_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | import os 4 | from collections import namedtuple 5 | 6 | from background_worker.task.remove_task import RemoveTask 7 | from background_worker.task.task import Task 8 | from server_utils.utils import SETTINGS_FOLDER 9 | import sys 10 | 11 | 12 | class TaskManager: 13 | def __init__(self, logging, notion_ai): 14 | self.tasks_path = SETTINGS_FOLDER + 'tasks.json' 15 | self.logging = logging 16 | self.notion_ai = notion_ai 17 | self.tasks_json = [] 18 | self.tasks = self._init_tasks() 19 | 20 | def _init_tasks(self): 21 | if os.path.isfile(self.tasks_path): 22 | print("Tasks json file found.") 23 | tasks = [] 24 | with open(self.tasks_path, 'r') as json_file: 25 | self.tasks_json = json.load(json_file) 26 | 27 | for task in self.tasks_json: 28 | if task["type"] == "RemoveTask": 29 | t = RemoveTask(dic=task, client=self.notion_ai.client) 30 | elif task["type"] == "Task": 31 | t = Task(dic=task) 32 | 33 | tasks.append(t) 34 | 35 | return tasks 36 | else: 37 | print("No tasks assigned yet.") 38 | return [] 39 | 40 | def find_task(self, task_to_find): 41 | for index, task in enumerate(self.tasks): 42 | if task.task_id == task_to_find.task_id: 43 | return index 44 | return -1 45 | 46 | def add_task(self, task): 47 | index = self.find_task(task) 48 | if index == -1: 49 | self.tasks_json.append(task.to_dic()) 50 | self.tasks.append(task) 51 | else: 52 | self.tasks_json[index] = task.to_dic() 53 | self.tasks[index] = task 54 | 55 | self.save_tasks_to_json() 56 | 57 | def remove_task(self, task): 58 | self.tasks.remove(task) 59 | self.save_tasks_to_json() 60 | 61 | def save_tasks_to_json(self): 62 | if len(self.tasks) == 0: 63 | os.remove(self.tasks_path) 64 | else: 65 | with open(self.tasks_path, 'w') as outfile: 66 | json.dump(self.tasks_json, outfile) 67 | -------------------------------------------------------------------------------- /Python-Server/app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | notionai-mymind: 5 | image: elblogbruno/notion-ai-mymind:latest 6 | ports: 7 | - 8000:5000 8 | -------------------------------------------------------------------------------- /Python-Server/app/image_tagging/clarifai_tagging/clarifai_tagging.py: -------------------------------------------------------------------------------- 1 | from clarifai_grpc.grpc.api import service_pb2_grpc, service_pb2, resources_pb2 2 | from clarifai_grpc.grpc.api.status import status_code_pb2 3 | from clarifai_grpc.channel.clarifai_channel import ClarifaiChannel 4 | 5 | 6 | class ClarifaiAI: 7 | 8 | def __init__(self, key): 9 | # Construct one of the channels you want to use 10 | self.channel = ClarifaiChannel.get_json_channel() 11 | self.key = key 12 | 13 | def get_tags(self, image_url, is_local_image, treshold): 14 | stub = service_pb2_grpc.V2Stub(self.channel) 15 | file_bytes = {} 16 | file = "" 17 | 18 | if is_local_image: 19 | file = "./uploads/" + image_url.split("/")[-1] 20 | with open(file, "rb") as f: 21 | file_bytes = f.read() 22 | else: 23 | file = image_url 24 | 25 | if is_local_image: 26 | request = service_pb2.PostModelOutputsRequest( 27 | model_id='aaa03c23b3724a16a56b629203edc62c', 28 | inputs=[ 29 | resources_pb2.Input(data=resources_pb2.Data(image=resources_pb2.Image(base64=file_bytes))) 30 | ]) 31 | else: 32 | request = service_pb2.PostModelOutputsRequest( 33 | model_id='aaa03c23b3724a16a56b629203edc62c', 34 | inputs=[ 35 | resources_pb2.Input(data=resources_pb2.Data(image=resources_pb2.Image(url=file))) 36 | ]) 37 | 38 | metadata = (('authorization', 'Key {0}'.format(self.key)),) 39 | 40 | response = stub.PostModelOutputs(request, metadata=metadata) 41 | 42 | if response.status.code != status_code_pb2.SUCCESS: 43 | raise Exception("Request failed, status code: " + str(response.status.code)) 44 | 45 | tags = [] 46 | for concept in response.outputs[0].data.concepts: 47 | if concept.value > treshold: 48 | tags.append(str(concept.name)) 49 | 50 | print('Predicted:', tags) 51 | 52 | return tags 53 | -------------------------------------------------------------------------------- /Python-Server/app/image_tagging/image_tagging.py: -------------------------------------------------------------------------------- 1 | from image_tagging.clarifai_tagging.clarifai_tagging import ClarifaiAI 2 | from image_tagging.tensorflow_tagging.tensorflow_tagging import TensorFlowTag 3 | from server_utils.utils import SETTINGS_FOLDER 4 | from notion_ai.property_manager.tag_object import TagObject 5 | from os.path import join 6 | 7 | import json 8 | 9 | 10 | # This class holds the image tagging system, and it acts as a parent for the Clarifai and Tensorflow tagging 11 | # mechanisms. 12 | 13 | class ImageTagging: 14 | 15 | def __init__(self, data, logging): 16 | options = {} 17 | self.logging = logging 18 | filename = SETTINGS_FOLDER + 'tagging_options.json' 19 | with open(filename) as json_file: 20 | options = json.load(json_file) 21 | try: 22 | self.options = options 23 | self.treshold = 0.20 24 | if 'confidence_treshold' in options: 25 | self.treshold = float(options['confidence_treshold']) 26 | if options['use_clarifai']: 27 | self.predictor = ClarifaiAI(data['clarifai_key']) 28 | self.logging.info("Using clarifai predictor") 29 | else: 30 | self.predictor = TensorFlowTag(options['delete_after_tagging']) 31 | self.logging.info("Using tensorflow predictor, with delete_after_tagging = {}".format( 32 | options['delete_after_tagging'])) 33 | except FileNotFoundError: 34 | self.logging.info("options.json not found") 35 | print("Wrong file or file path") 36 | 37 | def get_tags(self, image_url, is_image_local): 38 | self.print_current_detector() 39 | return self.predictor.get_tags(image_url, is_image_local, self.treshold) 40 | 41 | def print_current_detector(self): 42 | if self.options['use_clarifai']: 43 | self.logging.info("Using clarifai predictor with treshold: {}".format(self.treshold)) 44 | else: 45 | self.logging.info("Using tensorflow predictor, with delete_after_tagging = {0} and treshold: {1}".format( 46 | self.options['delete_after_tagging'], self.treshold)) 47 | 48 | def remove_duplicated_tags(self, lists_of_tags): 49 | res = [] 50 | for l in lists_of_tags: 51 | for i in l: 52 | if i not in res: 53 | res.append(i) 54 | return res 55 | 56 | def count_duplicated_tags(self, lists_of_tags): 57 | res = {} 58 | for tags in lists_of_tags: 59 | for tag in tags: 60 | if tag: 61 | if tag not in res: 62 | res[tag] = 1 63 | else: 64 | res[tag] = res[tag] + 1 65 | return res 66 | 67 | def get_most_used_ai_tags(self, notion_ai, collection_index, number_of_tags=10): 68 | print("Getting most tags") 69 | #notion_ai.mind_structure.set_current_collection(collection_index) 70 | 71 | rows = notion_ai.mind_structure.collection.get_rows() 72 | 73 | all_tags = [] 74 | 75 | for row in rows: 76 | ai_tags = notion_ai.property_manager.get_properties(row, ai_tags_property=1).split(",") 77 | if len(ai_tags) > 0: 78 | all_tags.append(ai_tags) 79 | 80 | freq = self.count_duplicated_tags(all_tags) 81 | 82 | sorted_freq = sorted(freq, key=freq.get, reverse=True) 83 | 84 | if len(sorted_freq) > 0: 85 | if number_of_tags > len(sorted_freq): 86 | number_of_tags = len(sorted_freq) 87 | 88 | for i in range(0, number_of_tags): 89 | sorted_freq[i] = TagObject(tag_value=sorted_freq[i]).to_dict() 90 | 91 | return sorted_freq 92 | -------------------------------------------------------------------------------- /Python-Server/app/image_tagging/tensorflow_tagging/tensorflow_tagging.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import numpy as np 6 | from PIL import UnidentifiedImageError 7 | 8 | from tensorflow.python.keras.applications.inception_v3 import InceptionV3, preprocess_input, decode_predictions 9 | from tensorflow.python.keras.preprocessing import image 10 | import os 11 | from server_utils.utils import download_image_from_url, createFolder 12 | 13 | 14 | class TensorFlowTag: 15 | 16 | def __init__(self, delete_image_after_tagging=True): 17 | self.delete_after_tagging = delete_image_after_tagging 18 | self.model = InceptionV3(weights='imagenet') 19 | 20 | def get_tags(self, image_url, is_local_image, treshold): 21 | if is_local_image: 22 | file = "./uploads/" + image_url.split("/")[-1] 23 | else: 24 | file = download_image_from_url(image_url) 25 | 26 | if file: 27 | try: 28 | img = image.load_img(file, target_size=(299, 299)) 29 | x = image.img_to_array(img) 30 | x = np.expand_dims(x, axis=0) 31 | x = preprocess_input(x) 32 | 33 | preds = self.model.predict(x) 34 | prediction_decoded = decode_predictions(preds, top=20)[0] 35 | 36 | print('Predicted:', prediction_decoded) 37 | 38 | tags = [] 39 | for element in prediction_decoded: 40 | if element[2] > treshold: 41 | tags.append(element[1]) 42 | 43 | if self.delete_after_tagging: 44 | os.remove(file) 45 | 46 | except UnidentifiedImageError as e: 47 | self.logging.info(str(e)) 48 | 49 | return tags 50 | else: 51 | return [] 52 | -------------------------------------------------------------------------------- /Python-Server/app/notion/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/Python-Server/app/notion/__init__.py -------------------------------------------------------------------------------- /Python-Server/app/notion/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from .settings import LOG_FILE 5 | 6 | 7 | NOTIONPY_LOG_LEVEL = os.environ.get("NOTIONPY_LOG_LEVEL", "warning").lower() 8 | 9 | logger = logging.getLogger("notion") 10 | 11 | 12 | def enable_debugging(): 13 | set_log_level(logging.DEBUG) 14 | 15 | 16 | def set_log_level(level): 17 | logger.setLevel(level) 18 | handler.setLevel(level) 19 | 20 | 21 | if NOTIONPY_LOG_LEVEL == "disabled": 22 | handler = logging.NullHandler() 23 | logger.addHandler(handler) 24 | else: 25 | handler = logging.FileHandler(LOG_FILE) 26 | formatter = logging.Formatter("\n%(asctime)s - %(levelname)s - %(message)s") 27 | handler.setFormatter(formatter) 28 | logger.addHandler(handler) 29 | 30 | if NOTIONPY_LOG_LEVEL == "debug": 31 | set_log_level(logging.DEBUG) 32 | elif NOTIONPY_LOG_LEVEL == "info": 33 | set_log_level(logging.INFO) 34 | elif NOTIONPY_LOG_LEVEL == "warning": 35 | set_log_level(logging.WARNING) 36 | elif NOTIONPY_LOG_LEVEL == "error": 37 | set_log_level(logging.ERROR) 38 | else: 39 | raise Exception( 40 | "Invalid value for environment variable NOTIONPY_LOG_LEVEL: {}".format( 41 | NOTIONPY_LOG_LEVEL 42 | ) 43 | ) 44 | -------------------------------------------------------------------------------- /Python-Server/app/notion/maps.py: -------------------------------------------------------------------------------- 1 | from inspect import signature 2 | 3 | from .logger import logger 4 | from .markdown import markdown_to_notion, notion_to_markdown 5 | 6 | 7 | class mapper(property): 8 | def __init__(self, path, python_to_api, api_to_python, *args, **kwargs): 9 | self.python_to_api = python_to_api 10 | self.api_to_python = api_to_python 11 | self.path = ( 12 | ".".join(map(str, path)) 13 | if isinstance(path, list) or isinstance(path, tuple) 14 | else path 15 | ) 16 | super().__init__(*args, **kwargs) 17 | 18 | 19 | def field_map(path, python_to_api=lambda x: x, api_to_python=lambda x: x): 20 | """ 21 | Returns a property that maps a Block attribute onto a field in the API data structures. 22 | 23 | - `path` can either be a top-level field-name, a list that specifies the key names to traverse, 24 | or a dot-delimited string representing the same traversal. 25 | 26 | - `python_to_api` is a function that converts values as given in the Python layer into the 27 | internal representation to be sent along in the API request. 28 | 29 | - `api_to_python` is a function that converts what is received from the API into an internal 30 | representation to be returned to the Python layer. 31 | """ 32 | 33 | if isinstance(path, str): 34 | path = path.split(".") 35 | 36 | def fget(self): 37 | kwargs = {} 38 | if ( 39 | "client" in signature(api_to_python).parameters 40 | and "id" in signature(api_to_python).parameters 41 | ): 42 | kwargs["client"] = self._client 43 | kwargs["id"] = self.id 44 | return api_to_python(self.get(path), **kwargs) 45 | 46 | def fset(self, value): 47 | kwargs = {} 48 | if "client" in signature(python_to_api).parameters: 49 | kwargs["client"] = self._client 50 | self.set(path, python_to_api(value, **kwargs)) 51 | 52 | return mapper( 53 | fget=fget, 54 | fset=fset, 55 | path=path, 56 | python_to_api=python_to_api, 57 | api_to_python=api_to_python, 58 | ) 59 | 60 | 61 | def property_map( 62 | name, python_to_api=lambda x: x, api_to_python=lambda x: x, markdown=True 63 | ): 64 | """ 65 | Similar to `field_map`, except it works specifically with the data under the "properties" field 66 | in the API's block table, and just takes a single name to specify which subkey to reference. 67 | Also, these properties all seem to use a special "embedded list" format that breaks the text 68 | up into a sequence of chunks and associated format metadata. If `markdown` is True, we convert 69 | this representation into commonmark-compatible markdown, and back again when saving. 70 | """ 71 | 72 | def py2api(x, client=None): 73 | kwargs = {} 74 | if "client" in signature(python_to_api).parameters: 75 | kwargs["client"] = client 76 | x = python_to_api(x, **kwargs) 77 | if markdown: 78 | x = markdown_to_notion(x) 79 | return x 80 | 81 | def api2py(x, client=None, id=""): 82 | x = x or [[""]] 83 | if markdown: 84 | x = notion_to_markdown(x) 85 | kwargs = {} 86 | params = signature(api_to_python).parameters 87 | if "client" in params: 88 | kwargs["client"] = client 89 | if "id" in params: 90 | kwargs["id"] = id 91 | return api_to_python(x, **kwargs) 92 | 93 | return field_map(["properties", name], python_to_api=py2api, api_to_python=api2py) 94 | 95 | 96 | def joint_map(*mappings): 97 | """ 98 | Combine multiple `field_map` and `property_map` instances together to map an attribute to multiple API fields. 99 | Note: when "getting", the first one will be used. When "setting", they will all be set in parallel. 100 | """ 101 | 102 | def fget(self): 103 | return mappings[0].fget(self) 104 | 105 | def fset(self, value): 106 | for m in mappings: 107 | m.fset(self, value) 108 | 109 | return property(fget=fget, fset=fset) 110 | -------------------------------------------------------------------------------- /Python-Server/app/notion/monitor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import requests 4 | import threading 5 | import time 6 | import uuid 7 | 8 | from collections import defaultdict 9 | from inspect import signature 10 | from requests import HTTPError 11 | 12 | from .collection import Collection 13 | from .logger import logger 14 | from .records import Record 15 | 16 | 17 | class Monitor(object): 18 | 19 | thread = None 20 | 21 | def __init__(self, client, root_url="https://msgstore.www.notion.so/primus/"): 22 | self.client = client 23 | self.session_id = str(uuid.uuid4()) 24 | self.root_url = root_url 25 | self._subscriptions = set() 26 | self.initialize() 27 | 28 | def _decode_numbered_json_thing(self, thing): 29 | 30 | thing = thing.decode().strip() 31 | 32 | for ping in re.findall('\d+:\d+"primus::ping::\d+"', thing): 33 | logger.debug("Received ping: {}".format(ping)) 34 | self.post_data(ping.replace("::ping::", "::pong::")) 35 | 36 | results = [] 37 | for blob in re.findall("\d+:\d+(\{.*?\})(?=\d|$)", thing): 38 | results.append(json.loads(blob)) 39 | if thing and not results and "::ping::" not in thing: 40 | logger.debug("Could not parse monitoring response: {}".format(thing)) 41 | return results 42 | 43 | def _encode_numbered_json_thing(self, data): 44 | assert isinstance(data, list) 45 | results = "" 46 | for obj in data: 47 | msg = str(len(obj)) + json.dumps(obj, separators=(",", ":")) 48 | msg = "{}:{}".format(len(msg), msg) 49 | results += msg 50 | return results.encode() 51 | 52 | def initialize(self): 53 | 54 | logger.debug("Initializing new monitoring session.") 55 | 56 | response = self.client.session.get( 57 | "{}?sessionId={}&EIO=3&transport=polling".format( 58 | self.root_url, self.session_id 59 | ) 60 | ) 61 | 62 | self.sid = self._decode_numbered_json_thing(response.content)[0]["sid"] 63 | 64 | logger.debug("New monitoring session ID is: {}".format(self.sid)) 65 | 66 | # resubscribe to any existing subscriptions if we're reconnecting 67 | old_subscriptions, self._subscriptions = self._subscriptions, set() 68 | self.subscribe(old_subscriptions) 69 | 70 | def subscribe(self, records): 71 | 72 | if isinstance(records, set): 73 | records = list(records) 74 | 75 | if not isinstance(records, list): 76 | records = [records] 77 | 78 | sub_data = [] 79 | 80 | for record in records: 81 | 82 | if record not in self._subscriptions: 83 | 84 | logger.debug( 85 | "Subscribing new record to the monitoring watchlist: {}/{}".format( 86 | record._table, record.id 87 | ) 88 | ) 89 | 90 | # add the record to the list of records to restore if we're disconnected 91 | self._subscriptions.add(record) 92 | 93 | # subscribe to changes to the record itself 94 | sub_data.append( 95 | { 96 | "type": "/api/v1/registerSubscription", 97 | "requestId": str(uuid.uuid4()), 98 | "key": "versions/{}:{}".format(record.id, record._table), 99 | "version": record.get("version", -1), 100 | } 101 | ) 102 | 103 | # if it's a collection, subscribe to changes to its children too 104 | if isinstance(record, Collection): 105 | sub_data.append( 106 | { 107 | "type": "/api/v1/registerSubscription", 108 | "requestId": str(uuid.uuid4()), 109 | "key": "collection/{}".format(record.id), 110 | "version": -1, 111 | } 112 | ) 113 | 114 | data = self._encode_numbered_json_thing(sub_data) 115 | 116 | self.post_data(data) 117 | 118 | def post_data(self, data): 119 | 120 | if not data: 121 | return 122 | 123 | logger.debug("Posting monitoring data: {}".format(data)) 124 | 125 | self.client.session.post( 126 | "{}?sessionId={}&transport=polling&sid={}".format( 127 | self.root_url, self.session_id, self.sid 128 | ), 129 | data=data, 130 | ) 131 | 132 | def poll(self, retries=10): 133 | logger.debug("Starting new long-poll request") 134 | try: 135 | response = self.client.session.get( 136 | "{}?sessionId={}&EIO=3&transport=polling&sid={}".format( 137 | self.root_url, self.session_id, self.sid 138 | ) 139 | ) 140 | response.raise_for_status() 141 | except HTTPError as e: 142 | try: 143 | message = "{} / {}".format(response.content, e) 144 | except: 145 | message = "{}".format(e) 146 | logger.warn( 147 | "Problem with submitting polling request: {} (will retry {} more times)".format( 148 | message, retries 149 | ) 150 | ) 151 | time.sleep(0.1) 152 | if retries <= 0: 153 | raise 154 | if retries <= 5: 155 | logger.error( 156 | "Persistent error submitting polling request: {} (will retry {} more times)".format( 157 | message, retries 158 | ) 159 | ) 160 | # if we're close to giving up, also try reinitializing the session 161 | self.initialize() 162 | self.poll(retries=retries - 1) 163 | 164 | self._refresh_updated_records( 165 | self._decode_numbered_json_thing(response.content) 166 | ) 167 | 168 | def _refresh_updated_records(self, events): 169 | 170 | records_to_refresh = defaultdict(list) 171 | 172 | for event in events: 173 | 174 | logger.debug( 175 | "Received the following event from the remote server: {}".format(event) 176 | ) 177 | 178 | if not isinstance(event, dict): 179 | continue 180 | 181 | if event.get("type", "") == "notification": 182 | 183 | key = event.get("key") 184 | 185 | if key.startswith("versions/"): 186 | 187 | match = re.match("versions/([^\:]+):(.+)", key) 188 | if not match: 189 | continue 190 | 191 | record_id, record_table = match.groups() 192 | 193 | local_version = self.client._store.get_current_version( 194 | record_table, record_id 195 | ) 196 | if event["value"] > local_version: 197 | logger.debug( 198 | "Record {}/{} has changed; refreshing to update from version {} to version {}".format( 199 | record_table, record_id, local_version, event["value"] 200 | ) 201 | ) 202 | records_to_refresh[record_table].append(record_id) 203 | else: 204 | logger.debug( 205 | "Record {}/{} already at version {}, not trying to update to version {}".format( 206 | record_table, record_id, local_version, event["value"] 207 | ) 208 | ) 209 | 210 | if key.startswith("collection/"): 211 | 212 | match = re.match("collection/(.+)", key) 213 | if not match: 214 | continue 215 | 216 | collection_id = match.groups()[0] 217 | 218 | self.client.refresh_collection_rows(collection_id) 219 | row_ids = self.client._store.get_collection_rows(collection_id) 220 | 221 | logger.debug( 222 | "Something inside collection {} has changed; refreshing all {} rows inside it".format( 223 | collection_id, len(row_ids) 224 | ) 225 | ) 226 | 227 | records_to_refresh["block"] += row_ids 228 | 229 | self.client.refresh_records(**records_to_refresh) 230 | 231 | def poll_async(self): 232 | if self.thread: 233 | # Already polling async; no need to have two threads 234 | return 235 | self.thread = threading.Thread(target=self.poll_forever, daemon=True) 236 | self.thread.start() 237 | 238 | def poll_forever(self): 239 | while True: 240 | try: 241 | self.poll() 242 | except Exception as e: 243 | logger.error("Encountered error during polling!") 244 | logger.error(e, exc_info=True) 245 | time.sleep(1) 246 | -------------------------------------------------------------------------------- /Python-Server/app/notion/operations.py: -------------------------------------------------------------------------------- 1 | from .utils import now 2 | 3 | 4 | def build_operation(id, path, args, command="set", table="block"): 5 | """ 6 | Data updates sent to the submitTransaction endpoint consist of a sequence of "operations". This is a helper 7 | function that constructs one of these operations. 8 | """ 9 | 10 | if isinstance(path, str): 11 | path = path.split(".") 12 | 13 | return {"id": id, "path": path, "args": args, "command": command, "table": table} 14 | 15 | 16 | def operation_update_last_edited(user_id, block_id): 17 | """ 18 | When transactions are submitted from the web UI, it also includes an operation to update the "last edited" 19 | fields, so we want to send those too, for consistency -- this convenience function constructs the operation. 20 | """ 21 | return { 22 | "args": { 23 | "last_edited_by_id": user_id, 24 | "last_edited_by_table": "notion_user", 25 | "last_edited_time": now(), 26 | }, 27 | "command": "update", 28 | "id": block_id, 29 | "path": [], 30 | "table": "block", 31 | } 32 | -------------------------------------------------------------------------------- /Python-Server/app/notion/records.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from .logger import logger 4 | from .operations import build_operation 5 | from .utils import extract_id, get_by_path 6 | 7 | 8 | class Record(object): 9 | 10 | # if a subclass has a list of ids that should be update when child records are removed, it should specify the key here 11 | child_list_key = None 12 | 13 | def __init__(self, client, id, *args, **kwargs): 14 | self._client = client 15 | self._id = extract_id(id) 16 | self._callbacks = [] 17 | if self._client._monitor is not None: 18 | self._client._monitor.subscribe(self) 19 | 20 | @property 21 | def id(self): 22 | return self._id 23 | 24 | @property 25 | def role(self): 26 | return self._client._store.get_role(self._table, self.id) 27 | 28 | def _str_fields(self): 29 | """ 30 | Determines the list of fields to include in the __str__ representation. Override and extend this in subclasses. 31 | """ 32 | return ["id"] 33 | 34 | def __str__(self): 35 | return ", ".join( 36 | [ 37 | "{}={}".format(field, repr(getattr(self, field))) 38 | for field in self._str_fields() 39 | if getattr(self, field, "") 40 | ] 41 | ) 42 | 43 | def __repr__(self): 44 | return "<{} ({})>".format(self.__class__.__name__, self) 45 | 46 | def refresh(self): 47 | """ 48 | Update the cached data for this record from the server (data for other records may be updated as a side effect). 49 | """ 50 | self._get_record_data(force_refresh=True) 51 | 52 | def _convert_diff_to_changelist(self, difference, old_val, new_val): 53 | changed_values = set() 54 | for operation, path, values in deepcopy(difference): 55 | path = path.split(".") if isinstance(path, str) else path 56 | if operation in ["add", "remove"]: 57 | path.append(values[0][0]) 58 | while isinstance(path[-1], int): 59 | path.pop() 60 | changed_values.add(".".join(map(str, path))) 61 | return [ 62 | ( 63 | "changed_value", 64 | path, 65 | (get_by_path(path, old_val), get_by_path(path, new_val)), 66 | ) 67 | for path in changed_values 68 | ] 69 | 70 | def add_callback(self, callback, callback_id=None, extra_kwargs={}): 71 | assert callable( 72 | callback 73 | ), "The callback must be a 'callable' object, such as a function." 74 | callback_obj = self._client._store.add_callback( 75 | self, callback, callback_id=callback_id, extra_kwargs=extra_kwargs 76 | ) 77 | self._callbacks.append(callback_obj) 78 | return callback_obj 79 | 80 | def remove_callbacks(self, callback_or_callback_id_prefix=None): 81 | print("removing callback " + callback_or_callback_id_prefix) 82 | if callback_or_callback_id_prefix is None: 83 | for callback_obj in list(self._callbacks): 84 | self._client._store.remove_callbacks( 85 | self._table, self.id, callback_or_callback_id_prefix=callback_obj 86 | ) 87 | self._callbacks = [] 88 | else: 89 | self._client._store.remove_callbacks( 90 | self._table, 91 | self.id, 92 | callback_or_callback_id_prefix=callback_or_callback_id_prefix, 93 | ) 94 | if callback_or_callback_id_prefix in self._callbacks: 95 | self._callbacks.remove(callback_or_callback_id_prefix) 96 | 97 | def _get_record_data(self, force_refresh=False): 98 | return self._client.get_record_data( 99 | self._table, self.id, force_refresh=force_refresh 100 | ) 101 | 102 | def get(self, path=[], default=None, force_refresh=False): 103 | """ 104 | Retrieve cached data for this record. The `path` is a list (or dot-delimited string) the specifies the field 105 | to retrieve the value for. If no path is supplied, return the entire cached data structure for this record. 106 | If `force_refresh` is set to True, we force_refresh the data cache from the server before reading the values. 107 | """ 108 | return get_by_path( 109 | path, self._get_record_data(force_refresh=force_refresh), default=default 110 | ) 111 | 112 | def set(self, path, value): 113 | """ 114 | Set a specific `value` (under the specific `path`) on the record's data structure on the server. 115 | """ 116 | self._client.submit_transaction( 117 | build_operation(id=self.id, path=path, args=value, table=self._table) 118 | ) 119 | 120 | def __eq__(self, other): 121 | return self.id == other.id 122 | 123 | def __ne__(self, other): 124 | return self.id != other.id 125 | 126 | def __hash__(self): 127 | return hash(self.id) 128 | -------------------------------------------------------------------------------- /Python-Server/app/notion/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | BASE_URL = "https://www.notion.so/" 5 | API_BASE_URL = BASE_URL + "api/v3/" 6 | SIGNED_URL_PREFIX = "https://www.notion.so/signed/" 7 | S3_URL_PREFIX = "https://s3-us-west-2.amazonaws.com/secure.notion-static.com/" 8 | S3_URL_PREFIX_ENCODED = "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/" 9 | DATA_DIR = os.environ.get( 10 | "NOTION_DATA_DIR", str(Path(os.path.expanduser("~")).joinpath(".notion-py")) 11 | ) 12 | CACHE_DIR = str(Path(DATA_DIR).joinpath("cache")) 13 | LOG_FILE = str(Path(DATA_DIR).joinpath("notion.log")) 14 | 15 | try: 16 | os.makedirs(DATA_DIR) 17 | except FileExistsError: 18 | pass 19 | 20 | try: 21 | os.makedirs(CACHE_DIR) 22 | except FileExistsError: 23 | pass 24 | -------------------------------------------------------------------------------- /Python-Server/app/notion/smoke_test.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from .client import * 4 | from .block import * 5 | from .collection import NotionDate 6 | 7 | 8 | def run_live_smoke_test(token_v2, parent_page_url_or_id): 9 | 10 | client = NotionClient(token_v2=token_v2) 11 | 12 | parent_page = client.get_block(parent_page_url_or_id) 13 | 14 | page = parent_page.children.add_new( 15 | PageBlock, 16 | title="Smoke test at {}".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")), 17 | ) 18 | 19 | print("Created base smoke test page at:", page.get_browseable_url()) 20 | 21 | col_list = page.children.add_new(ColumnListBlock) 22 | col1 = col_list.children.add_new(ColumnBlock) 23 | col2 = col_list.children.add_new(ColumnBlock) 24 | col1kid = col1.children.add_new( 25 | TextBlock, title="Some formatting: *italic*, **bold**, ***both***!" 26 | ) 27 | assert ( 28 | col1kid.title.replace("_", "*") 29 | == "Some formatting: *italic*, **bold**, ***both***!" 30 | ) 31 | assert col1kid.title_plaintext == "Some formatting: italic, bold, both!" 32 | col2.children.add_new(TodoBlock, title="I should be unchecked") 33 | col2.children.add_new(TodoBlock, title="I should be checked", checked=True) 34 | 35 | page.children.add_new(HeaderBlock, title="The finest music:") 36 | video = page.children.add_new(VideoBlock, width=100) 37 | video.set_source_url("https://www.youtube.com/watch?v=oHg5SJYRHA0") 38 | 39 | assert video in page.children 40 | assert col_list in page.children 41 | assert video in page.children.filter(VideoBlock) 42 | assert col_list not in page.children.filter(VideoBlock) 43 | 44 | # check that the parent does not yet consider this page to be backlinking 45 | assert page not in parent_page.get_backlinks() 46 | 47 | page.children.add_new(SubheaderBlock, title="A link back to where I came from:") 48 | alias = page.children.add_alias(parent_page) 49 | assert alias.is_alias 50 | assert not page.is_alias 51 | page.children.add_new( 52 | QuoteBlock, 53 | title="Clicking [here]({}) should take you to the same place...".format( 54 | page.parent.get_browseable_url() 55 | ), 56 | ) 57 | 58 | # check that the parent now knows about the backlink 59 | assert page in parent_page.get_backlinks() 60 | 61 | # ensure __repr__ methods are not breaking 62 | repr(page) 63 | repr(page.children) 64 | for child in page.children: 65 | repr(child) 66 | 67 | page.children.add_new( 68 | SubheaderBlock, title="The order of the following should be alphabetical:" 69 | ) 70 | 71 | B = page.children.add_new(BulletedListBlock, title="B") 72 | D = page.children.add_new(BulletedListBlock, title="D") 73 | C2 = page.children.add_new(BulletedListBlock, title="C2") 74 | C1 = page.children.add_new(BulletedListBlock, title="C1") 75 | C = page.children.add_new(BulletedListBlock, title="C") 76 | A = page.children.add_new(BulletedListBlock, title="A") 77 | 78 | D.move_to(C, "after") 79 | A.move_to(B, "before") 80 | C2.move_to(C) 81 | C1.move_to(C, "first-child") 82 | 83 | page.children.add_new(CalloutBlock, title="I am a callout", icon="🤞") 84 | 85 | cvb = page.children.add_new(CollectionViewBlock) 86 | cvb.collection = client.get_collection( 87 | client.create_record("collection", parent=cvb, schema=get_collection_schema()) 88 | ) 89 | cvb.title = "My data!" 90 | view = cvb.views.add_new(view_type="table") 91 | 92 | special_code = uuid.uuid4().hex[:8] 93 | 94 | # add a row 95 | row1 = cvb.collection.add_row() 96 | assert row1.person == [] 97 | row1.name = "Just some data" 98 | row1.title = "Can reference 'title' field too! " + special_code 99 | assert row1.name == row1.title 100 | row1.check_yo_self = True 101 | row1.estimated_value = None 102 | row1.estimated_value = 42 103 | row1.files = [ 104 | "https://www.birdlife.org/sites/default/files/styles/1600/public/slide.jpg" 105 | ] 106 | row1.tags = None 107 | row1.tags = [] 108 | row1.tags = ["A", "C"] 109 | row1.where_to = "https://learningequality.org" 110 | row1.category = "A" 111 | row1.category = "" 112 | row1.category = None 113 | row1.category = "B" 114 | 115 | start = datetime.strptime("2020-01-01 09:30", "%Y-%m-%d %H:%M") 116 | end = datetime.strptime("2020-01-05 20:45", "%Y-%m-%d %H:%M") 117 | timezone = "America/Los_Angeles" 118 | reminder = {"unit": "minute", "value": 30} 119 | row1.some_date = NotionDate(start, end=end, timezone=timezone, reminder=reminder) 120 | 121 | # add another row 122 | row2 = cvb.collection.add_row(person=client.current_user, title="Metallic penguins") 123 | assert row2.person == [client.current_user] 124 | assert row2.name == "Metallic penguins" 125 | row2.check_yo_self = False 126 | row2.estimated_value = 22 127 | row2.files = [ 128 | "https://www.picclickimg.com/d/l400/pict/223603662103_/Vintage-Small-Monet-and-Jones-JNY-Enamel-Metallic.jpg" 129 | ] 130 | row2.tags = ["A", "B"] 131 | row2.where_to = "https://learningequality.org" 132 | row2.category = "C" 133 | 134 | # check that options "C" have been added to the schema 135 | for prop in ["=d{|", "=d{q"]: 136 | assert cvb.collection.get("schema.{}.options.2.value".format(prop)) == "C" 137 | 138 | # check that existing options "A" haven't been affected 139 | for prop in ["=d{|", "=d{q"]: 140 | assert ( 141 | cvb.collection.get("schema.{}.options.0.id".format(prop)) 142 | == get_collection_schema()[prop]["options"][0]["id"] 143 | ) 144 | 145 | # Run a filtered/sorted query using the view's default parameters 146 | result = view.default_query().execute() 147 | assert row1 == result[0] 148 | assert row2 == result[1] 149 | assert len(result) == 2 150 | 151 | # query the collection directly 152 | assert row1 in cvb.collection.get_rows(search=special_code) 153 | assert row2 not in cvb.collection.get_rows(search=special_code) 154 | assert row1 not in cvb.collection.get_rows(search="penguins") 155 | assert row2 in cvb.collection.get_rows(search="penguins") 156 | 157 | # search the entire space 158 | assert row1 in client.search_blocks(search=special_code) 159 | assert row1 not in client.search_blocks(search="penguins") 160 | assert row2 not in client.search_blocks(search=special_code) 161 | assert row2 in client.search_blocks(search="penguins") 162 | 163 | # Run an "aggregation" query 164 | aggregations = [ 165 | {"property": "estimated_value", "aggregator": "sum", "id": "total_value"} 166 | ] 167 | result = view.build_query(aggregations=aggregations).execute() 168 | assert result.get_aggregate("total_value") == 64 169 | 170 | # Run a "filtered" query 171 | filter_params = { 172 | "filters": [ 173 | { 174 | "filter": { 175 | "value": { 176 | "type": "exact", 177 | "value": {"table": "notion_user", "id": client.current_user.id}, 178 | }, 179 | "operator": "person_does_not_contain", 180 | }, 181 | "property": "person", 182 | } 183 | ], 184 | "operator": "and", 185 | } 186 | result = view.build_query(filter=filter_params).execute() 187 | assert row1 in result 188 | assert row2 not in result 189 | 190 | # Run a "sorted" query 191 | sort_params = [{"direction": "ascending", "property": "estimated_value"}] 192 | result = view.build_query(sort=sort_params).execute() 193 | assert row1 == result[1] 194 | assert row2 == result[0] 195 | 196 | # Test that reminders and time zone's work properly 197 | row1.refresh() 198 | assert row1.some_date.start == start 199 | assert row1.some_date.end == end 200 | assert row1.some_date.timezone == timezone 201 | assert row1.some_date.reminder == reminder 202 | 203 | print( 204 | "Check it out and make sure it looks good, then press any key here to delete it..." 205 | ) 206 | input() 207 | 208 | _delete_page_fully(page) 209 | 210 | 211 | def _delete_page_fully(page): 212 | 213 | id = page.id 214 | 215 | parent_page = page.parent 216 | 217 | assert page.get("alive") == True 218 | assert page in parent_page.children 219 | page.remove() 220 | assert page.get("alive") == False 221 | assert page not in parent_page.children 222 | 223 | assert ( 224 | page.space_info 225 | ), "Page {} was fully deleted prematurely, as we can't get space info about it anymore".format( 226 | id 227 | ) 228 | 229 | page.remove(permanently=True) 230 | 231 | time.sleep(1) 232 | 233 | assert ( 234 | not page.space_info 235 | ), "Page {} was not really fully deleted, as we can still get space info about it".format( 236 | id 237 | ) 238 | 239 | 240 | def get_collection_schema(): 241 | return { 242 | "%9:q": {"name": "Check Yo'self", "type": "checkbox"}, 243 | "=d{|": { 244 | "name": "Tags", 245 | "type": "multi_select", 246 | "options": [ 247 | { 248 | "color": "orange", 249 | "id": "79560dab-c776-43d1-9420-27f4011fcaec", 250 | "value": "A", 251 | }, 252 | { 253 | "color": "default", 254 | "id": "002c7016-ac57-413a-90a6-64afadfb0c44", 255 | "value": "B", 256 | }, 257 | ], 258 | }, 259 | "=d{q": { 260 | "name": "Category", 261 | "type": "select", 262 | "options": [ 263 | { 264 | "color": "orange", 265 | "id": "59560dab-c776-43d1-9420-27f4011fcaec", 266 | "value": "A", 267 | }, 268 | { 269 | "color": "default", 270 | "id": "502c7016-ac57-413a-90a6-64afadfb0c44", 271 | "value": "B", 272 | }, 273 | ], 274 | }, 275 | "LL[(": {"name": "Person", "type": "person"}, 276 | "4Jv$": {"name": "Estimated value", "type": "number"}, 277 | "OBcJ": {"name": "Where to?", "type": "url"}, 278 | "TwR:": {"name": "Some Date", "type": "date"}, 279 | "dV$q": {"name": "Files", "type": "file"}, 280 | "title": {"name": "Name", "type": "title"}, 281 | } 282 | -------------------------------------------------------------------------------- /Python-Server/app/notion/space.py: -------------------------------------------------------------------------------- 1 | from .logger import logger 2 | from .maps import property_map, field_map 3 | from .records import Record 4 | 5 | 6 | class Space(Record): 7 | 8 | _table = "space" 9 | 10 | child_list_key = "pages" 11 | 12 | name = field_map("name") 13 | domain = field_map("domain") 14 | icon = field_map("icon") 15 | 16 | @property 17 | def pages(self): 18 | # The page list includes pages the current user might not have permissions on, so it's slow to query. 19 | # Instead, we just filter for pages with the space as the parent. 20 | return self._client.search_pages_with_parent(self.id) 21 | 22 | @property 23 | def users(self): 24 | user_ids = [permission["user_id"] for permission in self.get("permissions")] 25 | self._client.refresh_records(notion_user=user_ids) 26 | return [self._client.get_user(user_id) for user_id in user_ids] 27 | 28 | def _str_fields(self): 29 | return super()._str_fields() + ["name", "domain"] 30 | 31 | def add_page(self, title, type="page", shared=False): 32 | assert type in [ 33 | "page", 34 | "collection_view_page", 35 | ], "'type' must be one of 'page' or 'collection_view_page'" 36 | if shared: 37 | permissions = [{"role": "editor", "type": "space_permission"}] 38 | else: 39 | permissions = [ 40 | { 41 | "role": "editor", 42 | "type": "user_permission", 43 | "user_id": self._client.current_user.id, 44 | } 45 | ] 46 | page_id = self._client.create_record( 47 | "block", self, type=type, permissions=permissions 48 | ) 49 | page = self._client.get_block(page_id) 50 | page.title = title 51 | return page 52 | -------------------------------------------------------------------------------- /Python-Server/app/notion/user.py: -------------------------------------------------------------------------------- 1 | from .logger import logger 2 | from .maps import property_map, field_map 3 | from .records import Record 4 | 5 | 6 | class User(Record): 7 | 8 | _table = "notion_user" 9 | 10 | given_name = field_map("given_name") 11 | family_name = field_map("family_name") 12 | email = field_map("email") 13 | locale = field_map("locale") 14 | time_zone = field_map("time_zone") 15 | 16 | @property 17 | def full_name(self): 18 | return " ".join([self.given_name or "", self.family_name or ""]).strip() 19 | 20 | def _str_fields(self): 21 | return super()._str_fields() + ["email", "full_name"] 22 | -------------------------------------------------------------------------------- /Python-Server/app/notion/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import uuid 3 | 4 | from bs4 import BeautifulSoup 5 | from urllib.parse import urlparse, parse_qs, quote_plus, unquote_plus 6 | from datetime import datetime 7 | from slugify import slugify as _dash_slugify 8 | 9 | from .settings import BASE_URL, SIGNED_URL_PREFIX, S3_URL_PREFIX, S3_URL_PREFIX_ENCODED 10 | 11 | 12 | class InvalidNotionIdentifier(Exception): 13 | pass 14 | 15 | 16 | def now(): 17 | return int(datetime.now().timestamp() * 1000) 18 | 19 | 20 | def extract_id(url_or_id): 21 | """ 22 | Extract the block/page ID from a Notion.so URL -- if it's a bare page URL, it will be the 23 | ID of the page. If there's a hash with a block ID in it (from clicking "Copy Link") on a 24 | block in a page), it will instead be the ID of that block. If it's already in ID format, 25 | it will be passed right through. 26 | """ 27 | input_value = url_or_id 28 | if url_or_id.startswith(BASE_URL): 29 | url_or_id = ( 30 | url_or_id.split("#")[-1] 31 | .split("/")[-1] 32 | .split("&p=")[-1] 33 | .split("?")[0] 34 | .split("-")[-1] 35 | ) 36 | try: 37 | return str(uuid.UUID(url_or_id)) 38 | except ValueError: 39 | raise InvalidNotionIdentifier(input_value) 40 | 41 | 42 | def get_embed_data(source_url): 43 | 44 | return requests.get( 45 | "https://api.embed.ly/1/oembed?key=421626497c5d4fc2ae6b075189d602a2&url={}".format( 46 | source_url 47 | ) 48 | ).json() 49 | 50 | 51 | def get_embed_link(source_url): 52 | 53 | data = get_embed_data(source_url) 54 | 55 | if "html" not in data: 56 | return source_url 57 | 58 | url = list(BeautifulSoup(data["html"], "html.parser").children)[0]["src"] 59 | 60 | return parse_qs(urlparse(url).query)["src"][0] 61 | 62 | 63 | def add_signed_prefix_as_needed(url, client=None, id=""): 64 | 65 | if url is None: 66 | return 67 | 68 | if url.startswith(S3_URL_PREFIX): 69 | url = SIGNED_URL_PREFIX + quote_plus(url) + "?table=block&id=" + id 70 | if client: 71 | url = client.session.head(url).headers.get("Location") 72 | 73 | return url 74 | 75 | 76 | def remove_signed_prefix_as_needed(url): 77 | if url is None: 78 | return 79 | if url.startswith(SIGNED_URL_PREFIX): 80 | return unquote_plus(url[len(S3_URL_PREFIX) :]) 81 | elif url.startswith(S3_URL_PREFIX_ENCODED): 82 | parsed = urlparse(url.replace(S3_URL_PREFIX_ENCODED, S3_URL_PREFIX)) 83 | return "{}://{}{}".format(parsed.scheme, parsed.netloc, parsed.path) 84 | else: 85 | return url 86 | 87 | 88 | def slugify(original): 89 | return _dash_slugify(original).replace("-", "_") 90 | 91 | 92 | def get_by_path(path, obj, default=None): 93 | 94 | if isinstance(path, str): 95 | path = path.split(".") 96 | 97 | value = obj 98 | 99 | # try to traverse down the sequence of keys defined in the path, to get the target value if it exists 100 | try: 101 | for key in path: 102 | if isinstance(value, list): 103 | key = int(key) 104 | value = value[key] 105 | except (KeyError, TypeError, IndexError): 106 | value = default 107 | 108 | return value 109 | -------------------------------------------------------------------------------- /Python-Server/app/notion_ai/custom_errors.py: -------------------------------------------------------------------------------- 1 | class OnImageNotFound(Exception): 2 | def __init__(self, *args): 3 | if args: 4 | self.message = args[0] 5 | else: 6 | self.message = None 7 | # args[1].imageStatusCode = 409 8 | args[1].statusCode = 200 9 | 10 | def __str__(self): 11 | if self.message: 12 | return 'OnImageNotFound, {0} '.format(self.message) 13 | else: 14 | return 'OnImageNotFound has been raised' 15 | 16 | 17 | class OnUrlNotValid(Exception): 18 | def __init__(self, *args): 19 | if args: 20 | self.message = args[0] 21 | else: 22 | self.message = None 23 | args[1].statusCode = 500 24 | 25 | def __str__(self): 26 | if self.message: 27 | return 'OnImageUrlNotValid, {0} '.format(self.message) 28 | else: 29 | return 'OnImageUrlNotValid has been raised' 30 | 31 | 32 | class OnTokenV2NotValid(Exception): 33 | def __init__(self, *args): 34 | if args: 35 | self.message = args[0] 36 | else: 37 | self.message = None 38 | 39 | def __str__(self): 40 | if self.message: 41 | return 'OnTokenV2NotValid, {0} '.format(self.message) 42 | else: 43 | return 'OnTokenV2NotValid has been raised' 44 | 45 | 46 | class OnCollectionNotAvailable(Exception): 47 | def __init__(self, *args): 48 | if args: 49 | self.message = args[0] 50 | else: 51 | self.message = None 52 | 53 | def __str__(self): 54 | if self.message: 55 | return 'OnCollectionNotAvailable, {0} '.format(self.message) 56 | else: 57 | return 'OnCollectionNotAvailable has been raised' 58 | 59 | 60 | class OnServerNotConfigured(Exception): 61 | def __init__(self, *args): 62 | if args: 63 | self.message = args[0] 64 | else: 65 | self.message = None 66 | self.status_code = 404 67 | 68 | def __str__(self): 69 | if self.message: 70 | return 'OnServerNotConfigured, {0} '.format(self.message) 71 | else: 72 | return 'OnServerNotConfigured has been raised' 73 | 74 | 75 | class OnWebClipperError(Exception): 76 | def __init__(self, *args): 77 | if args: 78 | message = args[0] 79 | self.message = message['message'] 80 | else: 81 | self.message = None 82 | 83 | def __str__(self): 84 | if self.message: 85 | return '{0}'.format(self.message) 86 | else: 87 | return 'OnWebClipperError has been raised' -------------------------------------------------------------------------------- /Python-Server/app/notion_ai/mind_structure.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from notion_ai.custom_errors import OnCollectionNotAvailable, OnServerNotConfigured 4 | from notion_ai.utils import get_joined_url, create_json_response 5 | import json 6 | import os 7 | from server_utils.utils import SETTINGS_FOLDER 8 | import urllib.parse 9 | 10 | from notion.block import CollectionViewPageBlock 11 | 12 | 13 | class MindStructure: 14 | def __init__(self, notion_ai, client, data, logging): 15 | self.client = client 16 | self.data = data 17 | self.collection_index = -1 18 | self.logging = logging 19 | self.notion_ai = notion_ai 20 | 21 | self.loaded = False 22 | 23 | def set_current_collection(self, index=0, id=None): 24 | print("Getting collection at index {0}".format(index)) 25 | try: 26 | if hasattr(self, 'collection_index') and index == self.collection_index: 27 | print("Collection is the same as requested before. {0} {1}".format(self.collection_index, index)) 28 | return self.collection 29 | else: 30 | print("Collection is not the same as requested before. {0} {1}".format(self.collection_index, index)) 31 | collection_id, id = self.get_collection_by_index(int(index)) 32 | # collection is our database or "mind" in notion, as be have multiple, if not suplied, it will get 33 | # the first one as the priority one. 34 | self.collection = self.client.get_collection(collection_id=collection_id) 35 | self.current_mind_id = id 36 | self.collection_index = index 37 | 38 | return self.collection 39 | except requests.exceptions.HTTPError as e: 40 | print("Http error : " + str(e)) 41 | except OnCollectionNotAvailable as e: 42 | print("Mind error: " + str(e)) 43 | 44 | def get_collection_by_index(self, index=0): 45 | structure = self.get_mind_structure_from_json() 46 | if len(structure) > 0 and int(index) >= 0: 47 | try: 48 | collection_id = structure[index]["collection_id"] 49 | collection_block_page_id = structure[index]["collection_block_page_id"] 50 | return collection_id, collection_block_page_id 51 | except TypeError as e: 52 | self.logging.info(str(e)) 53 | else: 54 | self.logging.info("No structure or collection was found") 55 | raise OnCollectionNotAvailable 56 | 57 | def get_mind_structure(self, notion_ai, structure_url=None, return_response=True): 58 | if structure_url is None: 59 | try: 60 | structure_url = self.data['url'] 61 | except AttributeError as e: 62 | raise OnServerNotConfigured(str(e)) 63 | 64 | structure = self.client.get_block(structure_url) 65 | structure.refresh() 66 | available_connections = [] 67 | 68 | for child in structure.children: 69 | try: 70 | if self._is_correct_collection(child): 71 | x = { 72 | "collection_name": child.title, 73 | "collection_id": child.collection.id, 74 | "collection_url": get_joined_url(child.id), 75 | "collection_block_page_id": child.id, 76 | "collection_cover": self._process_cover(child.collection.cover, child.collection.id) 77 | } 78 | available_connections.append(x) 79 | else: 80 | self.logging.info(str("Other type of not collections found on page {0}".format(type(child)))) 81 | except AttributeError as e: 82 | self.logging.info(str(e)) 83 | 84 | if len(available_connections) > 0: 85 | self._save_mind_structure(available_connections) 86 | self.loaded = True 87 | else: 88 | self.loaded = False 89 | 90 | if return_response: 91 | return create_json_response(notion_ai, status_code=300, append_content=available_connections) 92 | return available_connections 93 | 94 | def _is_correct_collection(self, child): 95 | return isinstance(child, CollectionViewPageBlock) and hasattr(child, "title") and len(child.title) > 0 96 | 97 | # https://github.com/jamalex/notion-py/issues/324 98 | def _process_cover(self, cover, collection_id): 99 | url = cover 100 | if url: 101 | if "http" not in cover or "https" not in cover: 102 | url = "https://www.notion.so" + cover 103 | else: 104 | cover = urllib.parse.quote(cover) 105 | cover = cover.replace("/", "%2F") 106 | url = "https://www.notion.so/image/{0}?table=collection&id={1}&width=300&userId={2}&cache=v2".format( 107 | cover, collection_id, self.client.current_user.id) 108 | else: 109 | url = "https://images.unsplash.com/photo-1579546929662-711aa81148cf?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MXx8fGVufDB8fHw%3D&w=1000&q=80" 110 | return url 111 | 112 | def _save_mind_structure(self, available_connection): 113 | try: 114 | with open(SETTINGS_FOLDER + 'mind_structure.json', 'w') as f: 115 | json.dump(available_connection, f) 116 | except FileNotFoundError as e: 117 | raise OnServerNotConfigured(e) 118 | 119 | def get_number_of_collections(self): 120 | structure = self.get_mind_structure_from_json() 121 | return len(structure) 122 | 123 | def get_mind_structure_from_json(self): 124 | if os.path.isfile(SETTINGS_FOLDER + 'mind_structure.json'): 125 | print("Mind json file found.") 126 | structure = {} 127 | with open(SETTINGS_FOLDER + 'mind_structure.json') as json_file: 128 | structure = json.load(json_file) 129 | return structure 130 | else: 131 | print("Mind json file not found. Creating...") 132 | return self.get_mind_structure(notion_ai=self.notion_ai, return_response=False) 133 | -------------------------------------------------------------------------------- /Python-Server/app/notion_ai/property_manager/multi_tag_manager.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | from uuid import uuid1 3 | 4 | import json 5 | import requests 6 | 7 | from notion_ai.property_manager.tag_object import TagObject 8 | from notion_ai.utils import create_json_response 9 | from server_utils.utils import SETTINGS_FOLDER, DEFAULT_COLOR 10 | 11 | 12 | ## This class manages the multi-choice tag property on a mind element. We can get current tags and add tags. 13 | class MultiTagManager: 14 | def __init__(self, logging, client, mind_structure, options): 15 | self.logging = logging 16 | 17 | self.client = client 18 | self.mind_structure = mind_structure 19 | self.multi_tag_property = options['multi_tag_property'] 20 | 21 | def get_multi_select_tags(self,notion_ai, append_tags, collection_index=None): 22 | #self.mind_structure.set_current_collection(int(collection_index)) 23 | prop = self.multi_tag_property 24 | print(prop) 25 | collection_schema = self.mind_structure.collection.get("schema") 26 | prop_schema = next( 27 | (v for k, v in collection_schema.items() if v["name"] == prop), None 28 | ) 29 | if not prop_schema: 30 | raise ValueError( 31 | f'"{prop}" property does not exist on the collection!' 32 | ) 33 | if prop_schema["type"] != "multi_select": 34 | raise ValueError(f'"{prop}" is not a multi select property!') 35 | l = [] 36 | 37 | if "options" in prop_schema: 38 | for element in prop_schema["options"]: 39 | color = DEFAULT_COLOR 40 | print(element) 41 | if "color" in element: 42 | color = self._notion_color_to_hex(element["color"]) 43 | else: 44 | element["color"] = color 45 | 46 | x = TagObject().parse_from_notion_element(element=element, tag_color=color) 47 | 48 | l.append(x) 49 | 50 | l = l + append_tags 51 | return create_json_response(notion_ai, status_code=200, append_content=l) 52 | else: 53 | if len(append_tags) > 0: 54 | return create_json_response(notion_ai, status_code=200, append_content=append_tags) 55 | else: 56 | raise ValueError(f'"{prop}" property has no tags on it.') 57 | 58 | def _get_multi_select_tags_as_list(self, collection_index=0): 59 | self.mind_structure.set_current_collection(int(collection_index)) 60 | prop = self.multi_tag_property 61 | collection_schema = self.mind_structure.collection.get("schema") 62 | prop_schema = next( 63 | (v for k, v in collection_schema.items() if v["name"] == prop), None 64 | ) 65 | if not prop_schema: 66 | raise ValueError( 67 | f'"{prop}" property does not exist on the collection!' 68 | ) 69 | if prop_schema["type"] != "multi_select": 70 | raise ValueError(f'"{prop}" is not a multi select property!') 71 | 72 | l = [] 73 | 74 | if "options" in prop_schema: 75 | for element in prop_schema["options"]: 76 | l.append(element["value"]) 77 | return l 78 | else: 79 | return l 80 | 81 | def update_multi_select_tags(self,notion_ai, id, tags_json, collection_index=0, color=None): 82 | try: 83 | # self.mind_structure.set_current_collection(int(collection_index)) 84 | self.logging.info("Updating multi select tags for row {0} {1} {2}".format(id, tags_json, collection_index)) 85 | print("Updating multi select tags for row {0} {1} {2}".format(id, tags_json, collection_index)) 86 | block = self.client.get_block(id) 87 | 88 | current_tags_notion = self._get_multi_select_tags_as_list(collection_index) 89 | tag_to_add = [] 90 | 91 | for tag in tags_json: 92 | 93 | if tag['option_name'] not in current_tags_notion or len(current_tags_notion) == 0: 94 | print(tag['option_name'] + " is new") 95 | value = self.add_new_multi_select_value(self.multi_tag_property, tag['option_name'], tag['option_color']) 96 | else: 97 | print(tag['option_name'] + " already exists") 98 | value = tag['option_name'] 99 | tag_to_add.append(value) 100 | 101 | block.set_property(self.multi_tag_property, tag_to_add) 102 | 103 | if len(block.get_property(self.multi_tag_property)) > 0: 104 | return create_json_response(notion_ai, status_code=205, rowId=id) 105 | else: 106 | return create_json_response(notion_ai, status_code=206, rowId=id) 107 | 108 | except ValueError as e: 109 | self.logging.info(e) 110 | print(e) 111 | except requests.exceptions.HTTPError as e: 112 | print(e) 113 | self.logging.info(e) 114 | return create_json_response(notion_ai, status_code=e.response.status_code, rowId=id) 115 | 116 | def add_new_multi_select_value(self, prop, value, color=None): 117 | colors = [ 118 | "default", 119 | "gray", 120 | "brown", 121 | "orange", 122 | "yellow", 123 | "green", 124 | "blue", 125 | "purple", 126 | "pink", 127 | "red", 128 | ] 129 | """`prop` is the name of the multi select property.""" 130 | if color is None: 131 | color = choice(colors) 132 | else: 133 | color = self._hex_to_notion_color(color) 134 | 135 | print("Tag color for {0} will be {1}".format(value,color)) 136 | 137 | collection_schema = self.mind_structure.collection.get("schema") 138 | prop_schema = next( 139 | (v for k, v in collection_schema.items() if v["name"] == prop), None 140 | ) 141 | if not prop_schema: 142 | raise ValueError( 143 | f'"{prop}" property does not exist on the collection!' 144 | ) 145 | if prop_schema["type"] != "multi_select": 146 | raise ValueError(f'"{prop}" is not a multi select property!') 147 | 148 | if "options" in prop_schema: # if there are no options in it, means there's no tags. 149 | dupe = next( 150 | (o for o in prop_schema["options"] if o["value"] == value), None 151 | ) 152 | if dupe: 153 | print(f'"{value}" already exists in the schema!') 154 | print(dupe['value']) 155 | return dupe['value'] 156 | 157 | prop_schema["options"].append( 158 | {"id": str(uuid1()), "value": value, "color": color} 159 | ) 160 | self.mind_structure.collection.set("schema", collection_schema) 161 | return value 162 | else: 163 | prop_schema["options"] = [] 164 | prop_schema["options"].append({"id": str(uuid1()), "value": value, "color": color}) 165 | 166 | self.mind_structure.collection.set("schema", collection_schema) 167 | return value 168 | 169 | def _notion_color_to_hex(self,color_name): 170 | if color_name == None: 171 | return "#505558" 172 | elif color_name == "default": 173 | return "#505558" 174 | elif color_name == "gray": 175 | return "#6B6F71" 176 | elif color_name == "brown": 177 | return "#695B55" 178 | elif color_name == "orange": 179 | return "#9F7445" 180 | elif color_name == "yellow": 181 | return "#9F9048" 182 | elif color_name == "green": 183 | return "#467870" 184 | elif color_name == "blue": 185 | return "#487088" 186 | elif color_name == "purple": 187 | return "#6C598F" 188 | elif color_name == "pink": 189 | return "#904D74" 190 | elif color_name == "red": 191 | return "#9F5C58" 192 | 193 | def _hex_to_notion_color(self,color_name): 194 | if color_name == None: 195 | return "default" 196 | if color_name == "#505558": 197 | return "default" 198 | elif color_name == "#6B6F71": 199 | return "gray" 200 | elif color_name == "#695B55": 201 | return "brown" 202 | elif color_name == "#9F7445": 203 | return "orange" 204 | elif color_name == "#9F9048": 205 | return "yellow" 206 | elif color_name == "#467870": 207 | return "green" 208 | elif color_name == "#487088": 209 | return "blue" 210 | elif color_name == "#6C598F": 211 | return "purple" 212 | elif color_name == "#904D74": 213 | return "pink" 214 | elif color_name == "#9F5C58": 215 | return "red" 216 | 217 | -------------------------------------------------------------------------------- /Python-Server/app/notion_ai/property_manager/property_manager.py: -------------------------------------------------------------------------------- 1 | from notion_ai.property_manager.multi_tag_manager import MultiTagManager 2 | from server_utils.utils import SETTINGS_FOLDER 3 | import json 4 | 5 | 6 | class PropertyManager: 7 | def __init__(self, logging, client, mind_structure): 8 | self.logging = logging 9 | 10 | self.client = client 11 | self.mind_structure = mind_structure 12 | 13 | current_properties = {} 14 | with open(SETTINGS_FOLDER + 'properties.json') as json_file: 15 | current_properties = json.load(json_file) 16 | self.current_properties = current_properties 17 | self.multi_tag_manager = MultiTagManager(logging, self.client, self.mind_structure, current_properties) 18 | 19 | def update_properties(self, block, **kwargs): 20 | self.logging.info("Updating propertios {0} for this block {1}".format(kwargs.keys(), block.id)) 21 | for key, value in kwargs.items(): 22 | block.set_property(self.current_properties[str(key)], value) 23 | 24 | def get_properties(self, block, debug=False, **kwargs): 25 | if debug: 26 | self.logging.info("Getting properties {0} for this block {1}".format(kwargs.keys(), block.id)) 27 | for key, value in kwargs.items(): 28 | return block.get_property(self.current_properties[str(key)]) 29 | 30 | def get_property_name(self, block, **kwargs): 31 | self.logging.info("Getting properties {0} for this block {1}".format(kwargs.items().encode('utf8'), block.id)) 32 | for key, value in kwargs.items(): 33 | return self.current_properties[str(key)] 34 | -------------------------------------------------------------------------------- /Python-Server/app/notion_ai/property_manager/tag_object.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import random 3 | 4 | class TagObject: 5 | def __init__(self, tag_value=None, tag_id=None, tag_color=None): 6 | self.option_name = tag_value 7 | 8 | if tag_id is None: 9 | tag_id = str(uuid.uuid1()) 10 | if tag_color is None: 11 | tag_color = "#"+''.join([random.choice('0123456789ABCDEF') for j in range(6)]) 12 | 13 | x = { 14 | "option_name": tag_value, 15 | "option_id": tag_id, 16 | "option_color": tag_color, 17 | } 18 | 19 | self.dic = x 20 | 21 | def __str__(self): 22 | return self.dic 23 | 24 | def to_dict(self): 25 | return self.dic 26 | 27 | def value(self): 28 | return self.dic["option_name"] 29 | 30 | def id(self): 31 | return self.dic["option_id"] 32 | 33 | def color(self): 34 | return self.dic["option_color"] 35 | 36 | def parse_from_notion_element(self, element, tag_color): 37 | print("Parsing Notion Element Tag {0}".format(element)) 38 | 39 | x = { 40 | "option_name": element["value"], 41 | "option_id": element["id"], 42 | "option_color": tag_color, 43 | } 44 | 45 | return x 46 | -------------------------------------------------------------------------------- /Python-Server/app/notion_ai/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from translation.translation_manager import TranslationManager 4 | from .custom_errors import OnUrlNotValid, OnImageNotFound, OnServerNotConfigured, OnWebClipperError 5 | from server_utils.utils import open_website 6 | 7 | from notion.block import ImageBlock 8 | from time import sleep 9 | 10 | import requests 11 | import validators 12 | import socket 13 | 14 | 15 | 16 | 17 | ##Makes a web request to the notion web clipper API to add url's and returns the rowId 18 | 19 | def web_clipper_request(self, url, title, current_mind_id, logging): 20 | try: 21 | cookies = { 22 | 'token_v2': self.token_v2, 23 | } 24 | 25 | headers = { 26 | 'Content-Type': 'application/json', 27 | } 28 | if title == None: 29 | title = url 30 | 31 | is_well_formed = validators.url(url) 32 | 33 | if is_well_formed: 34 | url_object = { 35 | "url": url, 36 | "title": title 37 | } 38 | data_dict = { 39 | "type": "block", 40 | "blockId": "{}".format(current_mind_id), 41 | "property": "P#~d", 42 | "items": [url_object], 43 | "from": "chrome" 44 | } 45 | data = json.dumps(data_dict) 46 | 47 | self.logging.info(data) 48 | response = requests.post('https://www.notion.so/api/v3/addWebClipperURLs', headers=headers, cookies=cookies, 49 | data=data) 50 | response_text = response.text 51 | 52 | json_response = json.loads(response_text) 53 | 54 | if 'createdBlockIds' in json_response: 55 | rowId = json_response['createdBlockIds'][0] 56 | return rowId 57 | else: 58 | raise OnWebClipperError(json_response) 59 | else: 60 | raise OnUrlNotValid("Invalid url was sent", self) 61 | except KeyError as e: 62 | raise OnServerNotConfigured(e) 63 | 64 | 65 | ##Extracts all images from content as a list of url's 66 | def extract_image_from_content(self, page_content, row_id): 67 | print("Will look for images on {}".format(page_content)) 68 | list_of_img_url = [] 69 | ##This loop looks at all the page content and finds every image on it. 70 | for element in page_content: 71 | im = self.client.get_block(element) 72 | block_type = im.get('type') 73 | if block_type == 'image': 74 | list_of_img_url.append(im.source) 75 | elif len(im.children) > 0: 76 | for child in im.children: 77 | if isinstance(child, ImageBlock): 78 | list_of_img_url.append(child.source) 79 | 80 | if len(list_of_img_url) == 0 and self.counter == self.times_to_retry: 81 | self.counter = 0 82 | print("Thumbnail Image URL not found. Value is None") 83 | raise OnImageNotFound("Thumbnail Image URL not found. Value is None", self) 84 | elif len(list_of_img_url) > 0: 85 | self.counter = 0 86 | self.logging.info("These images were found: " + str(list_of_img_url)) 87 | return list_of_img_url 88 | else: 89 | row = self.client.get_block(row_id) 90 | row.refresh() 91 | content = row.get('content') 92 | sleep(0.15) 93 | self.counter += 1 94 | return extract_image_from_content(self, content, row_id) 95 | return list_of_img_url 96 | 97 | 98 | def create_json_response(notion_ai, status_code = None, error_sentence=None, rowId=None, custom_sentence=None, append_content=None, port=None, custom_url="https://github.com/elblogbruno/NotionAI-MyMind/wiki/Common-Issues"): 99 | notion_ai.logging.info("Creating json response.") 100 | # if error_sentence is None: 101 | # error_sentence = "No translation found" 102 | 103 | if hasattr(notion_ai, 'loaded') and notion_ai.loaded: 104 | url = custom_url 105 | 106 | block_title = "-1" 107 | block_attached_url = "-1" 108 | 109 | if status_code is None: 110 | if hasattr(notion_ai, "status_code"): 111 | status_code = notion_ai.status_code 112 | else: 113 | status_code = 404 114 | else: 115 | notion_ai.status_code = status_code 116 | 117 | if rowId is not None: 118 | url = get_joined_url(rowId) 119 | row = notion_ai.client.get_block(rowId) 120 | block_title = row.title 121 | block_attached_url = row.url 122 | 123 | text_response = error_sentence 124 | status_text = "error" 125 | 126 | if text_response is None: 127 | text_response, status_text = notion_ai.translation_manager.get_response_text(status_code) 128 | 129 | if text_response == "error": 130 | text_response = error_sentence 131 | 132 | if len(block_attached_url) == 0: 133 | block_attached_url = "-1" 134 | if notion_ai.counter <= notion_ai.times_to_retry: 135 | notion_ai.counter = notion_ai.counter + 1 136 | return create_json_response(notion_ai=notion_ai, error_sentence=error_sentence, status_code=status_code, rowId=rowId, custom_sentence=custom_sentence, append_content=append_content, port=port, custom_url=custom_url) 137 | 138 | if len(block_title) == 0: 139 | block_title = "-1" 140 | 141 | if custom_sentence: 142 | text_response = custom_sentence 143 | 144 | x = { 145 | "status_code": status_code, 146 | "text_response": text_response, 147 | "status_text": status_text, 148 | "block_url": url, 149 | "block_title": block_title, 150 | "block_attached_url": block_attached_url, 151 | "extra_content": "null", 152 | } 153 | 154 | if append_content: 155 | x["extra_content"] = append_content 156 | # convert into JSON: 157 | json_response = json.dumps(x, ensure_ascii=False) 158 | print(json_response) 159 | return json_response 160 | else: 161 | notion_ai.translation_manager = TranslationManager(notion_ai.logging, notion_ai.static_folder) # we initialize the translation manager 162 | text_response, status_text = notion_ai.translation_manager.get_response_text(status_code) 163 | 164 | x = { 165 | "status_code": status_code, 166 | "text_response": text_response, 167 | "status_text": status_text, 168 | "block_url": get_server_url(port), 169 | "block_title": "-1", 170 | "block_attached_url": "-1", 171 | "extra_content": "null", 172 | } 173 | # convert into JSON: 174 | json_response = json.dumps(x) 175 | return json_response 176 | 177 | 178 | # based on the machine doing the request we know which extension is being used 179 | def get_current_extension_name(platform): 180 | print("Extension request platform is {}".format(platform)) 181 | if platform is None or len(platform) == 0: 182 | print("Final platform is unknown") 183 | return "Unknown mind extension" 184 | elif 'dart' in platform: 185 | print("Final platform is android") 186 | return "phone-app-extension" 187 | else: 188 | print("Final platform is browser") 189 | return "browser-extension" 190 | 191 | 192 | def open_browser_at_start(self, port): 193 | final_url = get_server_url(port) 194 | 195 | print("You should go to the homepage and set the credentials. The url will open in your browser now. If can't " 196 | "access a browser you can access {0}".format(final_url)) 197 | 198 | self.logging.info("You should go to the homepage and set the credentials. The url will open in your browser " 199 | "now. If can't access a browser you can access {0}".format(final_url)) 200 | 201 | open_website(final_url) 202 | 203 | 204 | def get_server_url(port): 205 | hostname = socket.gethostname() 206 | local_ip = socket.gethostbyname(hostname) 207 | final_url = "http://{0}:{1}/".format(str(local_ip), str(port)) 208 | return final_url 209 | 210 | 211 | def get_joined_url(rowId): 212 | rowIdExtracted = rowId.split("-") 213 | str1 = ''.join(str(e) for e in rowIdExtracted) 214 | url = "https://www.notion.so/" + str1 215 | return url 216 | -------------------------------------------------------------------------------- /Python-Server/app/requirements.txt: -------------------------------------------------------------------------------- 1 | # To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file 2 | clarifai-grpc==6.7.1 3 | #git+https://github.com/elblogbruno/notion-py.git 4 | requests==2.24.0 5 | urllib3==1.25.10 6 | validators==0.17.1 7 | Werkzeug==1.0.1 8 | tensorflow>=2.4.0 9 | keras 10 | pillow 11 | Quart~=0.14.1 12 | numpy~=1.19.2 13 | schedule 14 | #notion-py requirements 15 | commonmark 16 | bs4 17 | tzlocal 18 | python-slugify 19 | dictdiffer 20 | cached-property 21 | -------------------------------------------------------------------------------- /Python-Server/app/server_utils/check_update.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from server_utils.utils import open_website,get_path_file 3 | from notion_ai.utils import create_json_response 4 | import configparser 5 | from os.path import join 6 | 7 | 8 | def replace_version(version): 9 | '''Replace version in file with new version''' 10 | config = configparser.ConfigParser() 11 | config.read('static/version.cfg') 12 | config['version']['server_version'] = version 13 | 14 | 15 | def get_version(static_folder): 16 | print(static_folder) 17 | config = configparser.ConfigParser() 18 | config.read(get_path_file('static/version.cfg')) 19 | return config['version']['server_version'] 20 | 21 | 22 | def check_update(logging, static_folder, notion=None, port=None, return_response=False): 23 | api_url = "https://api.github.com/repos/elblogbruno/NotionAI-MyMind/releases/latest" 24 | response = requests.get(api_url) 25 | 26 | json_response = response.json() 27 | try: 28 | curr_version = get_version(static_folder) 29 | new_version = json_response['tag_name'] 30 | print("Checking server update...") 31 | print("Current version {0} - New Version Found {1}".format(curr_version, new_version)) 32 | if new_version == curr_version or curr_version > new_version: 33 | print("Server is up to date") 34 | if return_response: 35 | url = "https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/{0}".format(curr_version) 36 | return create_json_response(notion, port=port, status_code=0, custom_url=url) 37 | else: 38 | print("New version available: " + new_version) 39 | url = "https://github.com/elblogbruno/NotionAI-MyMind/releases/tag/{0}".format(new_version) 40 | if return_response: 41 | return create_json_response(notion, port=port, status_code=1, custom_url=url) 42 | else: 43 | open_website(url) 44 | except KeyError as e: 45 | logging.error("Key error checking for update, maybe file does not exist? " + str(e)) 46 | except requests.exceptions.HTTPError as e: 47 | logging.error("HTTP Error checking for update, maybe github is down or you don't have any internet?" + str(e)) 48 | -------------------------------------------------------------------------------- /Python-Server/app/server_utils/handle_options_data.py: -------------------------------------------------------------------------------- 1 | from .utils import save_data, save_options, save_tagging_options, save_properties_name 2 | 3 | # This handles the data from the options formulary and saves it to .json files. 4 | 5 | 6 | def process_formulary(logging, data): 7 | use_clarifai = _process_data(logging, data) 8 | _process_tagging_options(logging, data, use_clarifai) 9 | _process_properties(logging, data) 10 | 11 | return data 12 | 13 | 14 | def _process_data(logging, data): 15 | notion_url = data['notion_url'] 16 | 17 | notion_token = data['notion_token'] 18 | 19 | if data['clarifai_key']: 20 | clarifai_key = data['clarifai_key'] 21 | save_data(logging, url=notion_url, token=notion_token, clarifai_key=clarifai_key) 22 | use_clarifai = True 23 | else: 24 | save_data(logging, url=notion_url, token=notion_token) 25 | use_clarifai = False 26 | 27 | save_options(logging, language_code=data['language_code']) 28 | 29 | return use_clarifai 30 | 31 | 32 | def _process_tagging_options(logging, data, use_clarifai): 33 | if "delete_after_tagging" in data: 34 | delete_after_tagging = data['delete_after_tagging'] 35 | else: 36 | delete_after_tagging = False 37 | 38 | confidence_treshold = 0.20 39 | 40 | if "confidence_treshold" in data: 41 | if data['confidence_treshold'] == "": 42 | confidence_treshold = 0.20 43 | else: 44 | confidence_treshold = data['confidence_treshold'] 45 | 46 | logging.info( 47 | "Current Tagging Options --> Delete after Tagging: {0} ,Confidence Treshold: {1} , Use Clarifai: {2}".format( 48 | delete_after_tagging, confidence_treshold, use_clarifai)) 49 | 50 | save_tagging_options(logging, use_clarifai=use_clarifai, delete_after_tagging=delete_after_tagging, 51 | confidence_treshold=confidence_treshold) 52 | 53 | 54 | def _process_properties(logging, data): 55 | multi_tag_property = 'Tags' 56 | if data['multi_tag_property']: 57 | multi_tag_property = data['multi_tag_property'] 58 | 59 | mind_extension_property = 'mind_extension' 60 | if data['mind_extension_property']: 61 | mind_extension_property = data['mind_extension_property'] 62 | 63 | ai_tags_property = 'AITagsText' 64 | if data['ai_tags_property']: 65 | ai_tags_property = data['ai_tags_property'] 66 | 67 | notion_date_property = 'reminder' 68 | if data['notion_date_property']: 69 | notion_date_property = data['notion_date_property'] 70 | 71 | logging.info( 72 | "Current properties --> Multi Tag Property: {0} , Mind extension Property: {1} , AI Tags Property: {2}, " 73 | "Notion Date Reminder Property: {3}".format( 74 | multi_tag_property, mind_extension_property, ai_tags_property, notion_date_property)) 75 | 76 | save_properties_name(logging, multi_tag_property=multi_tag_property, 77 | mind_extension_property=mind_extension_property, ai_tags_property=ai_tags_property, 78 | notion_date_property=notion_date_property) 79 | -------------------------------------------------------------------------------- /Python-Server/app/server_utils/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests # to get image from the web 3 | import shutil # to save it locally 4 | import uuid 5 | import webbrowser 6 | import os, re 7 | 8 | path = "/proc/self/cgroup" 9 | 10 | DEFAULT_COLOR = "#505558" 11 | UPLOAD_FOLDER = os.getcwd()+'/uploads/' 12 | SETTINGS_FOLDER = os.getcwd()+'/settings/' 13 | 14 | ALLOWED_EXTENSIONS = set( 15 | ['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'mp3', 'wav', 'ogg', 'mp4', 'mov', 'avi', 'm4v', '3gp']) 16 | ALLOWED_AUDIO_EXTENSIONS = set(['mp3', 'wav', 'ogg']) 17 | ALLOWED_VIDEO_EXTENSIONS = set(['mp4', 'mov', 'avi', 'm4v', '3gp']) 18 | 19 | 20 | def get_path_file(filename): 21 | import os 22 | import sys 23 | from os import chdir 24 | from os.path import join 25 | from os.path import dirname 26 | from os import environ 27 | 28 | if hasattr(sys, '_MEIPASS'): 29 | # PyInstaller >= 1.6 30 | chdir(sys._MEIPASS) 31 | filename = join(sys._MEIPASS, filename) 32 | elif '_MEIPASS2' in environ: 33 | # PyInstaller < 1.6 (tested on 1.5 only) 34 | chdir(environ['_MEIPASS2']) 35 | filename = join(environ['_MEIPASS2'], filename) 36 | # else: 37 | # chdir(dirname(sys.argv[0])) 38 | # filename = join(dirname(sys.argv[0]), filename) 39 | 40 | return filename 41 | 42 | 43 | def is_docker(): 44 | if not os.path.isfile(path): return False 45 | with open(path) as f: 46 | for line in f: 47 | if re.match("\d+:[\w=]+:/docker(-[ce]e)?/\w+", line): 48 | return True 49 | return False 50 | 51 | 52 | def ask_server_port(logging): 53 | if is_docker(): 54 | print("running on docker") 55 | return int("5000") 56 | else: 57 | filename = SETTINGS_FOLDER + 'port.json' 58 | if os.path.isfile(filename): 59 | logging.info("Initiating with a found port.json file.") 60 | 61 | with open(filename, 'r') as json_file: 62 | options = json.load(json_file) 63 | logging.info("Using {} port".format(options['port'])) 64 | return options['port'] 65 | 66 | else: 67 | print("Asking initially for a port.") 68 | 69 | logging.info("Asking initially for a port.") 70 | 71 | port = input("Which port you'd like to run the server on: ") 72 | 73 | logging.info("Using {} port".format(port)) 74 | 75 | options = { 76 | 'port': port 77 | } 78 | 79 | with open(filename, 'w') as outfile: 80 | json.dump(options, outfile) 81 | 82 | logging.info("Port saved succesfully!") 83 | 84 | return options['port'] 85 | 86 | 87 | def save_tagging_options(logging, **kwargs): 88 | logging.info("Saving options.") 89 | 90 | data = {} 91 | for key, value in kwargs.items(): 92 | if key == "confidence_treshold": 93 | data[key] = value 94 | elif isinstance(value, bool): 95 | data[key] = value 96 | else: 97 | data[key] = True if len(value) > 0 else False 98 | 99 | with open(SETTINGS_FOLDER + 'tagging_options.json', 'w') as outfile: 100 | json.dump(data, outfile) 101 | 102 | logging.info("Options saved succesfully!") 103 | 104 | 105 | def save_options(logging, **kwargs): 106 | logging.info("Saving options.") 107 | 108 | data = {} 109 | for key, value in kwargs.items(): 110 | data[key] = value 111 | 112 | with open(SETTINGS_FOLDER + 'options.json', 'w') as outfile: 113 | json.dump(data, outfile) 114 | 115 | logging.info("Options saved succesfully!") 116 | 117 | 118 | def save_properties_name(logging, **kwargs): 119 | logging.info("Saving properties.") 120 | data = {} 121 | 122 | for key, value in kwargs.items(): 123 | data[key] = value 124 | 125 | with open(SETTINGS_FOLDER + 'properties.json', 'w') as outfile: 126 | json.dump(data, outfile) 127 | 128 | logging.info("Properties saved succesfully!") 129 | 130 | 131 | def save_data(logging, **kwargs): 132 | logging.info("Saving data.") 133 | data = {} 134 | 135 | for key, value in kwargs.items(): 136 | data[key] = value 137 | 138 | with open(SETTINGS_FOLDER + 'data.json', 'w') as outfile: 139 | json.dump(data, outfile) 140 | 141 | logging.info("Data saved succesfully!") 142 | 143 | 144 | def append_data(logging, **kwargs): 145 | logging.info("Appending data.") 146 | with open(SETTINGS_FOLDER + "data.json", "r+") as file: 147 | current_data = json.load(file) 148 | data = {} 149 | for key, value in kwargs.items(): 150 | data[key] = value 151 | current_data.update(data) 152 | file.seek(0) 153 | json.dump(current_data, file) 154 | logging.info("Appending data succesfully!") 155 | 156 | 157 | def download_audio_from_url(audio_url): 158 | print("Downloading this {} audio".format(audio_url)) 159 | filename = "./uploads/" + str(uuid.uuid4()) 160 | if 'mp3' in audio_url: 161 | filename = filename + ".mp3" 162 | elif 'wav' in audio_url: 163 | filename = filename + ".wav" 164 | elif 'ogg' in audio_url: 165 | filename = filename + ".ogg" 166 | 167 | # Open the url audio, set stream to True, this will return the stream content. 168 | r = requests.get(audio_url, stream=True) 169 | 170 | # Check if the audio was retrieved successfully 171 | if r.status_code == 200: 172 | # Set decode_content value to True, otherwise the downloaded audio file's size will be zero. 173 | r.raw.decode_content = True 174 | 175 | # Open a local file with wb ( write binary ) permission. 176 | with open(filename, 'wb') as f: 177 | shutil.copyfileobj(r.raw, f) 178 | 179 | print('Audio sucessfully Downloaded: ', filename) 180 | else: 181 | print('Audio Couldn\'t be retreived') 182 | return filename 183 | 184 | 185 | def download_image_from_url(image_url): 186 | filename = UPLOAD_FOLDER + str(uuid.uuid4()) 187 | 188 | if 'png' in image_url: 189 | filename = filename + ".png" 190 | elif 'jpg' in image_url: 191 | filename = filename + ".jpg" 192 | elif 'gif' in image_url: 193 | filename = filename + ".gif" 194 | 195 | print("Downloading this {} image".format(image_url)) 196 | 197 | # Open the url image, set stream to True, this will return the stream content. 198 | r = requests.get(image_url, stream=True) 199 | 200 | # Check if the image was retrieved successfully 201 | if r.status_code == 200: 202 | # Set decode_content value to True, otherwise the downloaded image file's size will be zero. 203 | r.raw.decode_content = True 204 | 205 | # Open a local file with wb ( write binary ) permission. 206 | with open(filename, 'wb') as f: 207 | shutil.copyfileobj(r.raw, f) 208 | 209 | print('Image sucessfully Downloaded: ', filename) 210 | else: 211 | print('Image Couldn\'t be retreived') 212 | return None 213 | return filename 214 | 215 | 216 | def createFolder(directory): 217 | try: 218 | if not os.path.exists(directory): 219 | os.makedirs(directory) 220 | except OSError: 221 | print('Error: Creating directory. ' + directory) 222 | 223 | 224 | def open_website(final_url): 225 | webbrowser.open(final_url) 226 | 227 | 228 | def get_file_extension(filename): 229 | return filename.rsplit('.', 1)[1].lower() 230 | 231 | 232 | def is_a_video_file(filename): 233 | extension = get_file_extension(filename) 234 | return extension in ALLOWED_VIDEO_EXTENSIONS 235 | 236 | 237 | def is_a_sound_file(filename): 238 | extension = get_file_extension(filename) 239 | return extension in ALLOWED_AUDIO_EXTENSIONS 240 | 241 | 242 | def allowed_file(filename): 243 | return '.' in filename and \ 244 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 245 | -------------------------------------------------------------------------------- /Python-Server/app/static/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Credentials error! 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Error!

18 |

Notion Credentials are incorrect. Please check you have enter the correct tokenV2 or email and password.

19 |
20 |

21 | Having trouble? Post an issue on github 22 |

23 |

24 | Go back to settings 25 |

26 |

27 | Continue to repo's homepage 28 |

29 |

Made with love by @elblogbruno!

30 |
31 | 32 | -------------------------------------------------------------------------------- /Python-Server/app/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/Python-Server/app/static/icon.png -------------------------------------------------------------------------------- /Python-Server/app/static/thank_you.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Notion AI My Mind Thank You! 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Thank You!

18 |

You have now a fully 100% open source server with AI.

19 |

Server URL is:

20 |

21 |

Set this url on the browser extension and phone app.

22 |

If you have downloaded the iphone app, you can scan this QR Code, so you configure the app in seconds.

23 |
24 |
25 |

26 | Don't have any of these? Download the browser extension and app right now 27 |

28 |

29 | Having trouble? Post an issue on github 30 |

31 |

32 | Go back to settings 33 |

34 |

35 | Continue to repo's homepage 36 |

37 |

Made with love by @elblogbruno!

38 |
39 | 40 | 45 | -------------------------------------------------------------------------------- /Python-Server/app/static/translations/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentences": { 3 | "200": 4 | { 5 | "response": "Added to your mind.", 6 | "status": "success" 7 | }, 8 | "401": 9 | { 10 | "response": "No Notion credentials where provided. Please configure server before adding content.", 11 | "status": "error" 12 | }, 13 | "204": 14 | { 15 | "response": "Title and Url was modified correctly", 16 | "status": "success" 17 | }, 18 | "205": 19 | { 20 | "response": "Tags updated succesfully.", 21 | "status": "success" 22 | }, 23 | "206": 24 | { 25 | "response": "No Tags are available.", 26 | "status": "success" 27 | }, 28 | "429": 29 | { 30 | "response": "Too much requests to Notion API.", 31 | "status": "error" 32 | }, 33 | "400": 34 | { 35 | "response": "Invalid content provided.", 36 | "status": "error" 37 | }, 38 | "300": 39 | { 40 | "response": "Succesfully refreshed available collections.", 41 | "status": "success" 42 | }, 43 | "304": 44 | { 45 | "response": "Reminder set succesfully.", 46 | "status": "success" 47 | }, 48 | "305": 49 | { 50 | "response": "Invalid date for a reminder.", 51 | "status": "error" 52 | }, 53 | "403": 54 | { 55 | "response": "Invalid url or text was provided.", 56 | "status": "error" 57 | }, 58 | "1": 59 | { 60 | "response": "There is an update available for the server.", 61 | "status": "success" 62 | }, 63 | "0": 64 | { 65 | "response": "Server is up to date.", 66 | "status": "success" 67 | }, 68 | "2": 69 | { 70 | "response": "Notion Token V2 was updated succesfully!", 71 | "status": "success" 72 | }, 73 | "3": 74 | { 75 | "response": "Notion Token V2 could not be updated. Please do it manually!", 76 | "status": "success" 77 | }, 78 | "404": 79 | { 80 | "response": "Please configure the server. You can click this box to do so.", 81 | "status": "error" 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Python-Server/app/static/translations/es_ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentences": { 3 | "200": 4 | { 5 | "response": "Añadido a tu mente.", 6 | "status": "success" 7 | }, 8 | "401": 9 | { 10 | "response": "No se han proporcionado las credenciales de Notion. Por favor, configure el servidor antes de añadir contenido.", 11 | "status": "error" 12 | }, 13 | "204": 14 | { 15 | "response": "El título y la url se han modificado correctamente", 16 | "status": "success" 17 | }, 18 | "205": 19 | { 20 | "response": "Etiquetas actualizadas con éxito.", 21 | "status": "success" 22 | }, 23 | "206": 24 | { 25 | "response": "No hay etiquetas disponibles.", 26 | "status": "success" 27 | }, 28 | "429": 29 | { 30 | "response": "Demasiadas peticiones a la API de Notion.", 31 | "status": "error" 32 | }, 33 | "400": 34 | { 35 | "response": "Contenido no válido proporcionado.", 36 | "status": "error" 37 | }, 38 | "300": 39 | { 40 | "response": "Se han renovado con éxito las colecciones disponibles.", 41 | "status": "success" 42 | }, 43 | "304": 44 | { 45 | "response": "Recordatorio configurado correctamente.", 46 | "status": "success" 47 | }, 48 | "305": 49 | { 50 | "response": "Fecha no válida para recordatorio.", 51 | "status": "error" 52 | }, 53 | "403": 54 | { 55 | "response": "Se ha proporcionado una url o un texto no válido.", 56 | "status": "error" 57 | }, 58 | "1": 59 | { 60 | "response": "Hay una actualización disponible para el servidor.", 61 | "status": "success" 62 | }, 63 | "0": 64 | { 65 | "response": "El servidor está al día.", 66 | "status": "success" 67 | }, 68 | "2": 69 | { 70 | "response": "El Token V2 de Notion ha sido actualizado con éxito.", 71 | "status": "success" 72 | }, 73 | "3": 74 | { 75 | "response": "El Token V2 de Notion no pudo ser actualizado. Por favor, hágalo manualmente.", 76 | "status": "error" 77 | }, 78 | "404": 79 | { 80 | "response": "Por favor, configure el servidor. Puede hacer clic en esta casilla para hacerlo.", 81 | "status": "error" 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Python-Server/app/static/version.cfg: -------------------------------------------------------------------------------- 1 | [version] 2 | server_version = 2.0.6 3 | -------------------------------------------------------------------------------- /Python-Server/app/templates/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Notion AI My Mind Server Options! 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Notion AI My Mind Server

17 |
18 |

Options :

19 |
20 | 21 | 22 | 23 | Please set the https:// at start. Don't know what is this url? More info here: Notion AI My Mind Collections 24 | 25 |

Properties customization :

26 | If you have set custom names to properties, you can set them here. More info here 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |

Location Options :

35 |
36 | 37 | 41 |
42 | 43 |

Login options:

44 |

If you reload the server, you will need to enter the email and password again if using this credential system. It is so, so your password or email is not stored on the server. If you instead use the TokenV2 login method it will be saved and used on the next startup of the server.

45 |
46 | 47 | 48 | Server will never share your email with anyone else. 49 |
50 |
51 | 52 | 53 |
54 |

or

55 |
56 | 57 | 58 | This can be obtained looking at the cookies or the extension. More info here: Getting the credentials. 59 |
60 |

Image tagging options:

61 | AI Tagging Info 62 |
63 | 64 | 65 | This can be obtained for free at Clarifai website 66 |
67 |

If you enter a clarifai api key, clarifai will be used. (Clarifai is cloud base, non gpu dependant, image is tagged on clarifai's servers) 68 | if you leave it blank it will use local tensorflow (will use cores of your server but image will be processed locally)

69 |
70 | 71 | 72 |
73 |
74 | 75 | 76 | You can enter a number that goes from 0.01 to 1 (Being 1, 100%) This allows you to set the confidence of the AI tagging system tagging images. If blank default will be 0.2 (20%) 77 |
78 | 79 | 80 |

If you have downloaded the iphone app, you can scan this QR Code, so you configure the app in seconds.

81 |
82 |
83 |
84 | 85 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /Python-Server/app/translation/translation_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | from server_utils.utils import SETTINGS_FOLDER 3 | 4 | 5 | 6 | class TranslationManager: 7 | def __init__(self, logging, static_folder): 8 | self.language_code = self.get_current_language_code() 9 | self.TRANSLATIONS_FOLDER = "{0}/translations/".format(static_folder) 10 | self.logging = logging 11 | logging.info("Translation Manager created with this language code {0}".format(self.language_code)) 12 | 13 | def get_sentence_by_code(self, status_code): 14 | try: 15 | filename = "{0}{1}.json".format(self.TRANSLATIONS_FOLDER, self.language_code) 16 | translated_sentence = "error" 17 | status = "error" 18 | with open(filename, encoding='utf8') as json_file: 19 | data = json.load(json_file) 20 | translated_sentence = str(data['sentences'][str(status_code)]["response"]) 21 | status = str(data['sentences'][str(status_code)]["status"]) 22 | return translated_sentence, status 23 | except KeyError as e: 24 | self.logging.info("Error getting translation : " + str(e)) 25 | return "error", "error" 26 | 27 | def get_response_text(self, status_code): 28 | print("Sending response {}".format(status_code)) 29 | return self.get_sentence_by_code(status_code) 30 | 31 | def get_current_language_code(self): 32 | option_file = SETTINGS_FOLDER + "options.json" 33 | language_code = "en_US" 34 | try: 35 | with open(option_file) as option_file: 36 | data = json.load(option_file) 37 | language_code = data['language_code'] 38 | return language_code 39 | except FileNotFoundError as e: 40 | return language_code 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 5 | Logo 6 | 7 | 8 | 9 | 10 |

NotionAI MyMind

11 | 12 |

13 | This repo uses AI and the wonderful Notion to enable you to add anything on the web to your "Mind" and forget about everything else. 14 |
15 | Explore the docs » 16 |
17 |
18 | View Demo 19 | · 20 | Report Bug 21 | · 22 | Request Feature 23 |

24 |

25 | Get it on Google Play 26 | 27 |

28 | 29 | Logo 30 | 31 |

32 | 33 | ### Add content 34 | This is collections example, where you can have different collections or databases of contents, fully customizable on notion. » 35 | 36 | 37 | collections 38 | collections 39 | 40 | 41 | ### Search 42 | This is a fully customizable and searchable database in Notion. » 43 | 44 | 45 | collections 46 | 47 | 48 | # Free your mind! 49 | 50 | 51 |
52 | Table of Contents 53 |
    54 |
  1. 55 | Project Philosophy 56 | 61 |
  2. 62 |
  3. 63 | Installing 64 | 68 | 69 |
  4. 70 |
  5. Common Issues
  6. 71 |
  7. Roadmap
  8. 72 |
73 |
74 | 75 | ## Project Philosophy. 76 | 77 | The idea is to have extensions for your mind on the browser, and app on android and Ios, allowing you to add whatever you find on the web in your "Mind". Also, adding image and article tagging capabilities thanks to AI, so you can simply search on your "Mind" for what you remember. 78 | 79 | Right now, there's a working Python Local Server, that receives all the data from the extension and the app, and publishes it to your fully customizable and searchable database in Notion. So it is 100% open source and fully private! 80 | 81 | Maybe we can say it is an Open Source Alternative to [Raindrop](https://raindrop.io/) and [Microsoft Edge Collections](https://support.microsoft.com/en-us/microsoft-edge/organize-your-ideas-with-collections-in-microsoft-edge-60fd7bba-6cfd-00b9-3787-b197231b507e), but much cooler with Community driven opinion and AI Capabilities, and a repo maintainer with lot of imagination (yes my brain goes at 150% speed)! 82 | 83 | ## Examples of what you can do. 84 | 85 | Add text to your mind | Add images to your mind 86 | :---: | :---: 87 | ![](doc/add_text.gif) | ![](doc/add_image.gif) 88 | 89 | 90 | Add websites to your mind | Search on your mind 91 | :---: | :---: 92 | ![](doc/add_website.gif) | ![](doc/header_gif_search.gif) 93 | 94 | ### Extensions 95 | 96 | [mymind](https://mymind.com/) company, friendly asked me to remove the browser extension as it infringes its copyright. Until further re-design of the browser extension, extension will not be available for download on the Chrome Web Store and Firefox Addon Store. 97 | 98 | ### Android and Ios users 99 | 100 | Users can install the android app from android store. On Ios you can clone the flutter project and build the app. 101 | 102 | Get it on Google Play 103 | 104 | 105 | I won't be releasing the app on the Apple App Store, as I don't have an Apple Developer Account either Mac OS based computer. 106 | Meanwhile, you can clone the flutter project and build the app yourself. 107 | 108 | ## Multilanguage Support and Translation Collaboration 109 | 110 | Since version 2.0.4 NotionAI-MyMind has multilanguage support! Now server, phone app and extension is translated to English and Spanish! 111 | Would you like to have it translated into your own language? 112 | 113 | You can have more info on how to collaborate it helps out people from all comunities and languages access this amazing tool! 114 | More info here: 115 | 116 | https://github.com/elblogbruno/NotionAI-MyMind-Translations 117 | 118 | ## Installation Tutorial 119 | [![Installation Tutorial](https://img.youtube.com/vi/v2wWtCYED1U/0.jpg)](https://www.youtube.com/watch?v=v2wWtCYED1U) 120 | 121 | # Installing 122 | - It is very easy, and there are different ways from click to install one's to more advanced ones, in case you want to install it from source. 123 | 124 | - You can check it out on the wiki: [Installing the Notion AI My Mind Server](https://github.com/elblogbruno/NotionAI-MyMind/wiki/Installing-the-Notion-AI-My-Mind-Server) 125 | 126 | - This covers: 127 | - Notion AI My Mind Server installation. 128 | 129 | ### I have installed the server, what to do next? 130 | - If you don't enter the Notion credentials either create the notion page, you would be not having fun with it! 131 | 132 | - You can check it out on the wiki: [I have installed the server, what to do next?](https://github.com/elblogbruno/NotionAI-MyMind/wiki/I-have-installed-the-server,-what-to-do-next%3F) 133 | 134 | - This covers: 135 | - Notion database creation 136 | - Browser or app walktrough with explanations. 137 | 138 | - You can watch the video also on how to create the structure! 139 | 140 | [![Installation Tutorial](https://img.youtube.com/vi/sRn6Pk1PnSY/0.jpg)](https://www.youtube.com/watch?v=sRn6Pk1PnSY) 141 | 142 | ### Docker-Compose 143 | 144 | - You can check it out on the wiki: [Installing the Notion AI My Mind Server on Docker](https://github.com/elblogbruno/NotionAI-MyMind/wiki/Installing-the-Notion-AI-My-Mind-Server-on-Docker) 145 | 146 | - This covers: 147 | - Server installation as a Docker Image 148 | 149 | ## Common Issues 150 | 151 | - You can check it out on the wiki: [Common Issues](https://github.com/elblogbruno/NotionAI-MyMind/wiki/Common-Issues) 152 | 153 | ## Roadmap 154 | - You can check the roadmap here: https://github.com/elblogbruno/NotionAI-MyMind/projects/1 155 | -------------------------------------------------------------------------------- /doc/add_image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/add_image.gif -------------------------------------------------------------------------------- /doc/add_text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/add_text.gif -------------------------------------------------------------------------------- /doc/add_website.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/add_website.gif -------------------------------------------------------------------------------- /doc/clarifai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/clarifai.png -------------------------------------------------------------------------------- /doc/collections-example-notion-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/collections-example-notion-page.png -------------------------------------------------------------------------------- /doc/collections-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/collections-example.png -------------------------------------------------------------------------------- /doc/customize_properties_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/customize_properties_name.png -------------------------------------------------------------------------------- /doc/extension_howto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/extension_howto.png -------------------------------------------------------------------------------- /doc/get_structure_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/get_structure_url.png -------------------------------------------------------------------------------- /doc/getting_cookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/getting_cookie.png -------------------------------------------------------------------------------- /doc/header_collections_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/header_collections_example.gif -------------------------------------------------------------------------------- /doc/header_collections_example_modify_title_tags.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/header_collections_example_modify_title_tags.gif -------------------------------------------------------------------------------- /doc/header_gif_joined_updated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/header_gif_joined_updated.gif -------------------------------------------------------------------------------- /doc/header_gif_joined_updated_collections.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/header_gif_joined_updated_collections.gif -------------------------------------------------------------------------------- /doc/header_gif_search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/header_gif_search.gif -------------------------------------------------------------------------------- /doc/header_phone_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/header_phone_demo.gif -------------------------------------------------------------------------------- /doc/notion-database-howto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/notion-database-howto.png -------------------------------------------------------------------------------- /doc/options_python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/options_python.png -------------------------------------------------------------------------------- /doc/qr_code_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/qr_code_example.png -------------------------------------------------------------------------------- /doc/refresh-collections-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/refresh-collections-menu.png -------------------------------------------------------------------------------- /doc/server_url_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/server_url_example.png -------------------------------------------------------------------------------- /doc/settings_howto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/doc/settings_howto.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Exceptions to above rules. 44 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 45 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 916c3ac648aa0498a70f32b5fc4f6c51447628e3 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/README.md: -------------------------------------------------------------------------------- 1 | # notion_ai_my_mind 2 | 3 | A new Flutter application. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | def keystoreProperties = new Properties() 25 | def keystorePropertiesFile = rootProject.file('key.properties') 26 | if (keystorePropertiesFile.exists()) { 27 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 28 | } 29 | 30 | apply plugin: 'com.android.application' 31 | apply plugin: 'kotlin-android' 32 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 33 | 34 | android { 35 | compileSdkVersion 28 36 | 37 | sourceSets { 38 | main.java.srcDirs += 'src/main/kotlin' 39 | } 40 | 41 | lintOptions { 42 | disable 'InvalidPackage' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.elblogbruno.notion_ai_my_mind" 48 | minSdkVersion 20 49 | targetSdkVersion 29 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | signingConfigs { 54 | release { 55 | keyAlias keystoreProperties['keyAlias'] 56 | keyPassword keystoreProperties['keyPassword'] 57 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 58 | storePassword keystoreProperties['storePassword'] 59 | } 60 | } 61 | buildTypes { 62 | release { 63 | // TODO: Add your own signing config for the release build. 64 | // Signing with the debug keys for now, so `flutter run --release` works. 65 | signingConfig signingConfigs.release 66 | } 67 | } 68 | } 69 | 70 | flutter { 71 | source '../..' 72 | } 73 | 74 | dependencies { 75 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 76 | } 77 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 9 | 10 | 11 | 12 | 13 | 18 | 26 | 30 | 34 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/kotlin/com/elblogbruno/notion_ai_my_mind/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.elblogbruno.notion_ai_my_mind 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | notion_ai_my_mind 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: notion_ai_my_mind 2 | description: A new Flutter application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.6+7 19 | 20 | environment: 21 | sdk: ">=2.7.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | url_launcher: ^5.5.0 27 | http: ^0.13.3 28 | receive_sharing_intent: ^1.4.4 29 | #receive_sharing_intent: 30 | # path: ../receive_sharing_intent 31 | fluttertoast: ^3.1.0 32 | shared_preferences: ^0.5.8 33 | #flutter_automation: ^1.4.0 34 | random_color: ^1.0.5 35 | cached_network_image: ^2.5.1 36 | flutter_tagging: ^2.2.0+3 37 | qr_code_scanner: ^0.3.5 38 | i18n_extension: ^3.0.0 39 | #flutter_link_preview: ^1.5.6 40 | date_time_picker: "^2.0.0" 41 | metadata_fetch: ^0.4.1 42 | flutter_localizations: 43 | sdk: flutter 44 | 45 | # The following adds the Cupertino Icons font to your application. 46 | # Use with the CupertinoIcons class for iOS style icons. 47 | cupertino_icons: ^0.1.3 48 | 49 | dev_dependencies: 50 | flutter_test: 51 | sdk: flutter 52 | 53 | # For information on the generic Dart part of this file, see the 54 | # following page: https://dart.dev/tools/pub/pubspec 55 | 56 | # The following section is specific to Flutter. 57 | flutter: 58 | 59 | # The following line ensures that the Material Icons font is 60 | # included with your application, so that you can use the icons in 61 | # the material Icons class. 62 | uses-material-design: true 63 | 64 | # To add assets to your application, add an assets section, like this: 65 | # assets: 66 | # - images/a_dot_burr.jpeg 67 | # - images/a_dot_ham.jpeg 68 | 69 | # An image asset can refer to one or more resolution-specific "variants", see 70 | # https://flutter.dev/assets-and-images/#resolution-aware. 71 | 72 | # For details regarding adding assets from package dependencies, see 73 | # https://flutter.dev/assets-and-images/#from-packages 74 | assets: 75 | - assets/translations.json 76 | # To add custom fonts to your application, add a fonts section here, 77 | # in this "flutter" section. Each entry in this list should have a 78 | # "family" key with the font family name, and a "fonts" key with a 79 | # list giving the asset and other descriptors for the font. For 80 | # example: 81 | # fonts: 82 | # - family: Schyler 83 | # fonts: 84 | # - asset: fonts/Schyler-Regular.ttf 85 | # - asset: fonts/Schyler-Italic.ttf 86 | # style: italic 87 | # - family: Trajan Pro 88 | # fonts: 89 | # - asset: fonts/TrajanPro.ttf 90 | # - asset: fonts/TrajanPro_Bold.ttf 91 | # weight: 700 92 | # 93 | # For details regarding fonts from package dependencies, 94 | # see https://flutter.dev/custom-fonts/#from-packages 95 | -------------------------------------------------------------------------------- /flutter-android-ios-app/notion_ai_my_mind/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:notion_ai_my_mind/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elblogbruno/NotionAI-MyMind/f09bad87d508e15d999f5b96866c86d4962e41f0/icon.png -------------------------------------------------------------------------------- /readme-translations/README.es.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 5 | Logo 6 | 7 | 8 |

NotionAI MyMind

9 | 10 |

11 | Este repo utiliza la IA y el maravilloso Notion para permitirte añadir cualquier cosa de la web a tu "Mente" y olvidarte de todo lo demás. 12 |
13 | Explorar la documentación » 14 |
15 |
16 | Ver demostración 17 | · 18 | Informar de un error 19 | · 20 | Solicitar una función 21 |

22 |

23 | Get it on Chrome Web Store Get it on Firefox Add-On Store Get it on Google Play 24 | 25 |

26 | 27 | Logo 28 | 29 |

30 | 31 | ### Añadir contenido 32 | Este es el ejemplo de las colecciones, donde se pueden tener diferentes colecciones o bases de datos de contenidos, totalmente personalizables en Notion. » 33 | 34 | 35 | collections 36 | collections 37 | 38 | 39 | ### Buscar en 40 | Se trata de una base de datos totalmente personalizable y con capacidad de búsqueda en Notion. » 41 | 42 | 43 | collections 44 | 45 | 46 | # ¡Libera tu mente! 47 | 48 | 49 |
50 | Índice de contenidos 51 |
    52 |
  1. 53 | Filosofía del proyecto 54 | 59 |
  2. 60 |
  3. 61 | Instalación 62 | 66 | 67 |
  4. 68 |
  5. Problemas comunes
  6. 69 |
  7. Hoja de ruta
  8. 70 |
71 |
72 | 73 | ## Filosofía del proyecto. 74 | 75 | La idea es tener extensiones para tu mente en el navegador, y aplicación en android e Ios, permitiéndote añadir cualquier cosa que encuentres en la web en tu "Mente". También, añadir capacidades de etiquetado de imágenes y artículos gracias a la IA, para que puedas simplemente buscar en tu "Mente" lo que recuerdes. 76 | 77 | En este momento, hay un servidor local de Python en funcionamiento, que recibe todos los datos de la extensión y la aplicación, y los publica en su base de datos totalmente personalizable y con capacidad de búsqueda en Notion. ¡Así que es 100% de código abierto y totalmente privado! 78 | 79 | ¡Tal vez podemos decir que es una alternativa de código abierto a [Raindrop](https://raindrop.io/) y [Microsoft Edge Collections](https://support.microsoft.com/en-us/microsoft-edge/organize-your-ideas-with-collections-in-microsoft-edge-60fd7bba-6cfd-00b9-3787-b197231b507e), pero mucho más fresco con la opinión de la comunidad y las capacidades que ofrece la Inteligencia Artificial, y un desarrollador con mucha imaginación (sí, mi cerebro va a 150% de velocidad)! 80 | 81 | ## Ejemplos de lo que puedes hacer. 82 | 83 | Añade texto a tu mente | Añade imágenes a tu mente 84 | :---: | :---: 85 | ![](../doc/add_text.gif) | ![](../doc/add_image.gif) 86 | 87 | 88 | Añade sitios web a tu mente | Busca en tu mente 89 | :---: | :---: 90 | ![](../doc/add_website.gif) | ![](../doc/header_gif_search.gif) 91 | 92 | ### Extensiones de la mente 93 | ### Chromium users 94 | ¡Los navegadores basados en chromium como Google Chrome,Brave o Microsoft edge entre otros, pueden instalar la extensión desde la tienda! 95 | 96 | Get it on Chrome Web Store 97 | 98 | ### Usuarios de Firefox 99 | Los usuarios de Firefox pueden instalar la extensión desde la tienda. 100 | 101 | Get it on Firefox Add-On Store 102 | 103 | ### Usuarios de Android e Ios 104 | 105 | Los usuarios pueden instalar la aplicación para Android desde la tienda de Android. En Ios se puede clonar el proyecto flutter y construir la aplicación. 106 | 107 | Get it on Google Play 108 | 109 | 110 | No voy a publicar la aplicación en la App Store de Apple, ya que no tengo una cuenta de desarrollador de Apple ni un ordenador basado en Mac OS. 111 | Mientras tanto, puedes clonar el proyecto Flutter y construir la aplicación tú mismo. 112 | 113 | ## Soporte multilingüe y colaboración en la traducción 114 | 115 | ¡Desde la versión 2.0.4 NotionAI-MyMind tiene soporte multi-idioma! ¡Ahora el servidor, la aplicación de teléfono y la extensión está traducida al inglés y al español! 116 | ¿Te gustaría tenerlo traducido a tu propio idioma? 117 | 118 | Puedes tener más información sobre cómo colaborar para que personas de todas las comunidades e idiomas puedan acceder a esta increíble herramienta. 119 | Más información aquí: 120 | 121 | https://github.com/elblogbruno/NotionAI-MyMind-Translations 122 | 123 | ## Tutorial de instalación 124 | [![Tutorial de instalación](https://img.youtube.com/vi/v2wWtCYED1U/0.jpg)](https://www.youtube.com/watch?v=v2wWtCYED1U) 125 | 126 | # Instalación 127 | - Es muy fácil, y hay diferentes formas, desde las de click to install hasta las más avanzadas, en caso de que quieras instalarlo desde el código fuente. 128 | 129 | - Puedes comprobarlo en la wiki: [Instalación del servidor de Notion AI My Mind](https://github.com/elblogbruno/NotionAI-MyMind/wiki/Installing-the-Notion-AI-My-Mind-Server) 130 | 131 | - Esto cubre: 132 | - Instalación del servidor Notion AI My Mind. 133 | 134 | ### He instalado el servidor, ¿qué hacer a continuación? 135 | - Si no introduces las credenciales de Notion ni creas tu primera pagina en Notion, ¡no estaras disfrutando de todo esto! 136 | 137 | - Puedes comprobarlo en la wiki: [He instalado el servidor, ¿qué hacer a continuación?](https://github.com/elblogbruno/NotionAI-MyMind/wiki/I-have-installed-the-server,-what-to-do-next%3F) 138 | 139 | - Esto cubre: 140 | - Creación de la base de datos de Notion 141 | - Paseo por el navegador o la aplicación con explicaciones. 142 | - Creación de la estructura de tu mente con las distintas colecciones dentro. 143 | 144 | - También puedes ver el vídeo sobre cómo crear la estructura de tu mente. 145 | 146 | [![Tutorial de instalación](https://img.youtube.com/vi/sRn6Pk1PnSY/0.jpg)](https://www.youtube.com/watch?v=sRn6Pk1PnSY) 147 | 148 | ### Docker-Compose 149 | 150 | - Puedes comprobarlo en la wiki: [Instalación del servidor de Notion AI My Mind en Docker](https://github.com/elblogbruno/NotionAI-MyMind/wiki/Installing-the-Notion-AI-My-Mind-Server-on-Docker) 151 | 152 | - Esto cubre: 153 | - La instalación del servidor como una imagen Docker 154 | 155 | ## Problemas comunes 156 | 157 | - Puedes comprobarlo en la wiki: [Problemas comunes](https://github.com/elblogbruno/NotionAI-MyMind/wiki/Common-Issues) 158 | 159 | ## Hoja de ruta 160 | - Puede consultar la hoja de ruta aquí: https://github.com/elblogbruno/NotionAI-MyMind/projects/1 161 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ `id -u` -ne 0 ] 4 | then 5 | echo "Please run this script with root privileges!" 6 | echo "Try again with sudo." 7 | exit 0 8 | fi 9 | 10 | echo "This script will install NotionAI My Mind Server" 11 | echo "NotionAI My Mind will install necessary dependencies for program to work" 12 | echo "Do you wish to continue? (y/n)" 13 | 14 | while true; do 15 | read -p "" yn 16 | case $yn in 17 | [Yy]* ) break;; 18 | [Nn]* ) exit 0;; 19 | * ) echo "Please answer with Yes or No [y|n].";; 20 | esac 21 | done 22 | 23 | echo "" 24 | echo "============================================================" 25 | echo "" 26 | echo "Installing necessary dependencies... (This could take a while)" 27 | echo "" 28 | echo "============================================================" 29 | apt-get update 30 | apt-get install -y python-pip git jq python3-pip python3.6 31 | echo "============================================================" 32 | if [ "$?" = "1" ] 33 | then 34 | echo "An unexpected error occured during apt-get!" 35 | exit 0 36 | fi 37 | 38 | 39 | 40 | echo "" 41 | echo "============================================================" 42 | echo "" 43 | echo "Cloning project from GitHub.." 44 | echo "" 45 | echo "============================================================" 46 | 47 | if ! [ -x "$(command -v pip3)" ]; then 48 | echo 'Error: PIP software for python3 (pip3) is not installed. I will install it for you!' >&2 49 | curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py" 50 | python3 get-pip.py --user 51 | fi 52 | 53 | git clone https://github.com/elblogbruno/NotionAI-MyMind 54 | 55 | cd NotionAI-MyMind/Python-Server/app && pip -r install requirements.txt 56 | 57 | if [ "$?" = "1" ] 58 | then 59 | echo "An unexpected error occured during pip install!" 60 | exit 0 61 | fi 62 | 63 | echo "============================================================" 64 | echo "Setup was successful." 65 | echo "You can run 'python server.py ' to start the server!" 66 | echo "Next steps are configuring the notion credentials!" 67 | echo "============================================================" 68 | 69 | sleep 2 70 | 71 | 72 | exit 0 73 | --------------------------------------------------------------------------------