From 8d3e24f4438c739e8e0f2fd0db338906d3865718 Mon Sep 17 00:00:00 2001 From: James Ravenscroft Date: Sun, 6 Nov 2022 07:14:33 +0000 Subject: [PATCH] reorganise plugins and add date arg to journal cmd --- poetry.lock | 57 ++++++++++++- pyproject.toml | 2 + src/rafael/Untitled-1.ipynb | 69 ++++++++-------- src/rafael/__init__.py | 41 ++-------- src/rafael/journal.py | 154 +++++++++++++++++++++++++++++++++++ src/rafael/util/__init__.py | 0 src/rafael/util/bookstack.py | 88 ++++++++++++++++++++ src/rafael/webmentions.py | 2 + 8 files changed, 344 insertions(+), 69 deletions(-) create mode 100644 src/rafael/journal.py create mode 100644 src/rafael/util/__init__.py create mode 100644 src/rafael/util/bookstack.py create mode 100644 src/rafael/webmentions.py diff --git a/poetry.lock b/poetry.lock index c885cc2..304fa38 100644 --- a/poetry.lock +++ b/poetry.lock @@ -118,6 +118,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "feedparser" +version = "6.0.10" +description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +sgmllib3k = "*" + [[package]] name = "flask" version = "2.1.1" @@ -174,6 +185,29 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "pendulum" +version = "2.1.2" +description = "Python datetimes made easy" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +python-dateutil = ">=2.6,<3.0" +pytzdata = ">=2020.1" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "0.20.0" @@ -224,6 +258,14 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" [package.dependencies] tzdata = {version = "*", markers = "python_version >= \"3.6\""} +[[package]] +name = "pytzdata" +version = "2020.1" +description = "The Olson timezone database for Python." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "requests" version = "2.27.1" @@ -242,6 +284,14 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "sgmllib3k" +version = "1.0.0" +description = "Py3k port of sgmllib." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "six" version = "1.16.0" @@ -329,7 +379,7 @@ watchdog = ["watchdog"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "aaeab26d780c5079dc962e4c086cbde44389716a5c642c36bedfaeaea3fd7621" +content-hash = "2e482c8851ca14e2a5a862af70863b9875e65584bbaf49d048fcc446091d92ad" [metadata.files] apscheduler = [ @@ -371,6 +421,7 @@ dokuwiki = [ {file = "dokuwiki-1.3.2-py3-none-any.whl", hash = "sha256:0720b9fef9cbcb01879b2723b65a5f5bef63a68e9bfc362b09b2e3d9ba7cdedb"}, {file = "dokuwiki-1.3.2.tar.gz", hash = "sha256:af01bf878da69d915bf15a003cd9ef0d364a706a87acf74d00c6bb337636d350"}, ] +feedparser = [] flask = [ {file = "Flask-2.1.1-py3-none-any.whl", hash = "sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264"}, {file = "Flask-2.1.1.tar.gz", hash = "sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8"}, @@ -429,6 +480,8 @@ markupsafe = [ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] +pendulum = [] +python-dateutil = [] python-dotenv = [ {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, @@ -445,10 +498,12 @@ pytz-deprecation-shim = [ {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, ] +pytzdata = [] requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] +sgmllib3k = [] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index 2baf0a7..78b37b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,8 @@ python-telegram-bot = "^13.11" todoist-api-python = "^1.1.1" python-dotenv = "^0.20.0" bs4 = "^0.0.1" +feedparser = "^6.0.10" +pendulum = "^2.1.2" [tool.poetry.dev-dependencies] diff --git a/src/rafael/Untitled-1.ipynb b/src/rafael/Untitled-1.ipynb index fdb7912..ef71be6 100644 --- a/src/rafael/Untitled-1.ipynb +++ b/src/rafael/Untitled-1.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -31,10 +31,38 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "r = session.get(\"https://wiki.jamesravey.me/api/books\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 Microcosm\n", + "2 PhD\n", + "4 House Stuff\n", + "5 Personal Stuff\n", + "7 🌱 Digital Garden Seed Propagator\n", + "8 ML and Data Science\n", + "9 Software Engineering\n", + "10 Devices and Tech\n", + "11 Journal\n" + ] + } + ], + "source": [ + "for book in r.json()['data']:\n", + " print(book['id'], book['name'])" + ] }, { "cell_type": "code", @@ -45,28 +73,7 @@ }, { "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "# Bookmarks\n", - "\n", - "\n", - "\n", - "[example](http://www.google.com)\n" - ] - } - ], - "source": [ - "print(r.json()['markdown'])" - ] - }, - { - "cell_type": "code", - "execution_count": 26, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -75,18 +82,12 @@ "" ] }, - "execution_count": 26, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], - "source": [ - "r = session.get(\"https://wiki.jamesravey.me/api/pages/58\")\n", - "\n", - "session.put(\"https://wiki.jamesravey.me/api/pages/58\", json={\n", - " \"markdown\": r.json()['markdown'] + \"\\n\\n - [example](http://www.google.com)\"\n", - "})" - ] + "source": [] }, { "cell_type": "code", diff --git a/src/rafael/__init__.py b/src/rafael/__init__.py index aab8e85..1a52ac6 100644 --- a/src/rafael/__init__.py +++ b/src/rafael/__init__.py @@ -1,7 +1,10 @@ +from collections import defaultdict import dotenv import os -import dokuwiki -import requests +import io +import tempfile + +from datetime import datetime from flask import Flask @@ -10,11 +13,13 @@ from telegram import Update, PhotoSize, File, BotCommand from telegram.ext import Updater, MessageHandler, CommandHandler, Filters, CallbackContext, ConversationHandler + app = Flask("rafael") RAFAEL_UA = "RAFAEL/0.1" from rafael.bookmarks import RafaelBookmarkPlugin +from rafael.journal import JournalPlugin def handle(update: Update, context: CallbackContext): @@ -40,39 +45,7 @@ def handle(update: Update, context: CallbackContext): update.message.reply_text("done m8") -ADDING = 0 -class JournalPlugin(): - - def init_convo(self, update: Update, context: CallbackContext): - update.message.reply_text("Add photos for your journal") - return ADDING - - def handle_file(self, update: Update, context: CallbackContext): - update.message.reply_text("Got your update m8") - return ADDING - - def done_handler(self, update: Update, context: CallbackContext): - update.message.reply_text("Great storing your images...") - return ConversationHandler.END - - def invalid_handler(self, update: Update, context: CallbackContext): - update.message.reply_text("Sorry, I don't understand. Please upload photos or type 'Done' to exit") - - def register(self, updater: Updater): - - updater.dispatcher.add_handler(ConversationHandler( - entry_points=[ - CommandHandler("journal", self.init_convo) - ], - states={ - ADDING: [MessageHandler(Filters.attachment, self.handle_file)] - }, - fallbacks=[ - MessageHandler(Filters.regex("^Done$"), self.done_handler), - MessageHandler(Filters.all, self.invalid_handler) - ] - )) class RafaelBot: diff --git a/src/rafael/journal.py b/src/rafael/journal.py new file mode 100644 index 0000000..6b3b0aa --- /dev/null +++ b/src/rafael/journal.py @@ -0,0 +1,154 @@ +import os +import io + +import pendulum +from datetime import datetime + +from collections import defaultdict + +from telegram import Update, PhotoSize, File, BotCommand +from telegram.ext import Updater, MessageHandler, CommandHandler, Filters, CallbackContext, ConversationHandler + +from rafael.util.bookstack import BookstackClient, Page + + +SELECT_DATE, ADDING = range(2) + +class JournalPlugin(): + + + def init_convo(self, update: Update, context: CallbackContext): + + print("Args:", context.args) + + journal_date = datetime.now() + + if len(context.args) > 0 : + try: + if context.args[0] == "yesterday": + journal_date = pendulum.yesterday() + + elif context.args[0] == "tomorrow": + journal_date = pendulum.tomorrow() + + else: + journal_date = pendulum.parse(context.args[0]) + except: + update.message.reply_text("""If you're passing a journal date you must use YYYY/MM/DD + You can also use 'yesterday' or 'tomorrow'""") + return + + + self.api = BookstackClient(os.getenv('BOOKSTACK_URL'), os.getenv('BOOKSTACK_TOKEN_ID'), os.getenv('BOOKMARK_TOKEN_SECRET')) + + chapter_name = journal_date.strftime("%B %Y") + + + page_name = journal_date.strftime("%Y-%m-%d") + + chapters = self.api.get_chapters(filters={"name":chapter_name, "book_id": os.getenv('BOOKSTACK_JOURNAL_BOOK')})['data'] + + chapter = chapters[0] + + pages = self.api.get_pages(filters={"name": page_name, "chapter_id": chapter['id']})['data'] + + if len(pages)<1: + print("Create Page") + + page = self.api.create_page(Page(chapter_id=chapter['id'], name=page_name, markdown=f"Journal for {page_name}")) + + context.chat_data['page_id'] = page['id'] + + print(page) + else: + print("Update Page") + + context.chat_data['page_id'] = pages[0]['id'] + + print(f"Use page {context.chat_data['page_id']}") + + + + update.message.reply_text("Journal Mode On: Add text and files") + return ADDING + + def handle_md_update(self, update: Update, context: CallbackContext): + + page = self.api.get_page(context.chat_data['page_id']) + + content = page['markdown'] + f"\n\n----------\n\n{update.message.text_markdown}" + + self.api.update_page(Page(id=page['id'], markdown=content)) + + update.message.reply_text(f"Added text to journal page {page['id']}") + + + def handle_file(self, update: Update, context: CallbackContext): + + if update.message.document is not None: + + file_meta = update.message.document.get_file() + file_name = update.message.document.file_name + + elif update.message.effective_attachment: + + versions = defaultdict(lambda:[]) + + biggest = 0 + biggest_i = -1 + + for i, att in enumerate(update.message.effective_attachment): + + if isinstance(att, PhotoSize): + + if att.width > biggest: + biggest = att.width + biggest_i = i + + + file_meta = update.message.effective_attachment[i].get_file() + file_name = update.message.effective_attachment[i].get_file().file_id + + + buf = io.BytesIO(file_meta.download_as_bytearray()) + + r = self.api.add_attachment_from_upload(context.chat_data['page_id'], file_name, buf) + + page = self.api.get_page(context.chat_data['page_id']) + + content = page['markdown'] + "\n\n-------\n\n" + f"![{r['name']}]({self.api.wiki_url}/attachments/{r['id']}?open=true)" + + self.api.update_page(Page(id=page['id'], markdown=content)) + + update.message.reply_text(f"Added file attachment id={r['id']} name={r['name']}") + + + + return ADDING + + def done_handler(self, update: Update, context: CallbackContext): + update.message.reply_text("Journal mode off") + return ConversationHandler.END + + def invalid_handler(self, update: Update, context: CallbackContext): + update.message.reply_text("Sorry, I don't understand. Please upload photos or type 'Done' to exit") + + def register(self, updater: Updater): + + updater.dispatcher.add_handler(ConversationHandler( + entry_points=[ + CommandHandler("journal", self.init_convo) + ], + states={ + ADDING: [ + MessageHandler(Filters.attachment, self.handle_file), + + MessageHandler(Filters.regex("^Done$"), self.done_handler), + MessageHandler(Filters.all, self.handle_md_update), + ] + }, + fallbacks=[ + MessageHandler(Filters.regex("^Done$"), self.done_handler), + + ] + )) \ No newline at end of file diff --git a/src/rafael/util/__init__.py b/src/rafael/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/rafael/util/bookstack.py b/src/rafael/util/bookstack.py new file mode 100644 index 0000000..3f7cd3c --- /dev/null +++ b/src/rafael/util/bookstack.py @@ -0,0 +1,88 @@ +from typing import BinaryIO, List, Optional +import requests +import dataclasses + +@dataclasses.dataclass +class Tag: + name: str + value: Optional[str] + +@dataclasses.dataclass +class Page: + + id: Optional[int] = dataclasses.field(default=None) + book_id: Optional[int] = dataclasses.field(default=None) + chapter_id: Optional[int] = dataclasses.field(default=None) + name: Optional[str] = dataclasses.field(default=None) + html: Optional[str] = dataclasses.field(default=None) + markdown: Optional[str] = dataclasses.field(default=None) + tags: List[Tag] = dataclasses.field(default=None) + +class BookstackClient: + + def __init__(self, url, token_id, token_secret): + + self.wiki_url = url + self.token_id = token_id + self.token_secret = token_secret + + + self.session = requests.Session() + self.session.headers = {'Authorization': f"Token {self.token_id}:{self.token_secret}"} + + + def _get(self, datatype, filters={}): + + query = {} + for key,value in filters.items(): + query[f'filter[{key}]']=value + + + r = self.session.get(self.wiki_url + f"/api/{datatype}", params=query) + + return r.json() + + def add_attachment_from_url(self, page_id: int, name: str, link: str): + + r = self.session.post(self.wiki_url + f"/api/attachments", + data={'name':name, 'uploaded_to':page_id, 'link':link} + ) + + return r.json() + + def add_attachment_from_upload(self, page_id: int, name: str, file: BinaryIO): + + r = self.session.post(self.wiki_url + f"/api/attachments", + data={'name':name, 'uploaded_to':page_id}, + files={'file': file} + ) + + return r.json() + + + + def get_pages(self, filters={}): + return self._get('pages', filters) + + def get_page(self, id: int): + r = self.session.get(self.wiki_url + f"/api/pages/{id}") + return r.json() + + def create_page(self, page: Page): + + r = self.session.post(self.wiki_url + "/api/pages", data=dataclasses.asdict(page) ) + return r.json() + + def update_page(self, page: Page): + r = self.session.put(self.wiki_url + f"/api/pages/{page.id}", data=dataclasses.asdict(page) ) + return r.json() + + def get_chapters(self, filters={}): + return self._get('chapters', filters) + + def get_books(self, filters={}): + return self._get('books', filters) + + + def get_shelves(self, filters={}): + return self._get('shelves', filters) diff --git a/src/rafael/webmentions.py b/src/rafael/webmentions.py new file mode 100644 index 0000000..ef30d3c --- /dev/null +++ b/src/rafael/webmentions.py @@ -0,0 +1,2 @@ +import feedparser +