diff --git a/app/activitypub.py b/app/activitypub.py
index f5884a9..dd445c9 100644
--- a/app/activitypub.py
+++ b/app/activitypub.py
@@ -75,6 +75,32 @@ ME = {
"tag": [] # TODO tag support
}
+class BaseActor:
+ def __init__(self, ap_actor: RawObject) -> None:
+ if (ap_type := ap_actor.get("type")) not in ACTOR_TYPES:
+ raise ValueError(f"Unexpected actor type: {ap_type}")
+
+ self._ap_actor = ap_actor
+ self._ap_type : str = ap_type # type: ignore
+
+ @property
+ def ap_actor(self) -> RawObject:
+ return self._ap_actor
+
+ @property
+ def inbox_url(self) -> str:
+ return self.ap_actor["inbox"]
+
+ @property
+ def ap_type(self) -> str:
+ return self._ap_type
+
+ @property
+ def share_inbox_url(self) -> str:
+ return self.ap_actor.get("endpoints", {}).get("sharedInbox") \
+ or self.inbox_url
+
+
class VisibilityEnum(str, enum.Enum):
PUBLIC = "public"
UNLISTED = "unlisted"
@@ -95,6 +121,26 @@ def handle_visibility(
return VisibilityEnum.DIRECT
+def wrap_ap_object(ap_object: dict) -> dict:
+ if ap_object["type"] in ["Note"]:
+
+ if "@context" in ap_object:
+ del ap_object["@context"]
+
+ return {
+ "@context": AS_EXTENDED_CTX,
+ "actor": config.ID,
+ "to": ap_object.get("to", []),
+ "cc": ap_object.get("cc", []),
+ "id": ap_object["id"] + "/activity",
+ "object": ap_object,
+ "published": ap_object["published"],
+ "type": "Create",
+ }
+
+ return ap_object
+
+
async def post(
url: str,
payload : dict,
@@ -112,6 +158,7 @@ async def post(
auth=auth,
)
+ resp.raise_for_status()
return resp
diff --git a/app/orgpython/LICENSE b/app/orgpython/LICENSE
new file mode 100644
index 0000000..d8f50df
--- /dev/null
+++ b/app/orgpython/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2017-2020, honmaple
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/app/orgpython/__init__.py b/app/orgpython/__init__.py
new file mode 100644
index 0000000..89ef442
--- /dev/null
+++ b/app/orgpython/__init__.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# ********************************************************************************
+# Copyright © 2017-2020 jianglin
+# File Name: __init__.py
+# Author: jianglin
+# Email: mail@honmaple.com
+# Created: 2019-05-29 18:06:22 (CST)
+# Last Update: Sunday 2020-08-16 19:45:09 (CST)
+# By:
+# Description:
+# ********************************************************************************
+from .document import Document
+
+
+def to_text(content, **kwargs):
+ return Document(content, **kwargs).to_text()
+
+
+def to_html(content, **kwargs):
+ return Document(content, **kwargs).to_html()
+
+
+def to_markdown(content, **kwargs):
+ return Document(content, **kwargs).to_markdown()
diff --git a/app/orgpython/document.py b/app/orgpython/document.py
new file mode 100644
index 0000000..f961544
--- /dev/null
+++ b/app/orgpython/document.py
@@ -0,0 +1,879 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# ********************************************************************************
+# Copyright © 2017-2020 jianglin
+# File Name: document.py
+# Author: jianglin
+# Email: mail@honmaple.com
+# Created: 2018-02-26 11:44:43 (CST)
+# Last Update: Wednesday 2020-08-19 12:00:03 (CST)
+# Description:
+# ********************************************************************************
+import re
+from hashlib import sha1
+from textwrap import dedent
+
+from .inline import Blankline, Hr, InlineText
+from .src import highlight as src_highlight
+
+DRAWER_BEGIN_REGEXP = re.compile(r"^(\s*):(\S+):\s*$")
+DRAWER_END_REGEXP = re.compile(r"^(\s*):END:\s*$")
+DRAWER_PROPERTY_REGEXP = re.compile(r"^(\s*):(\S+):(\s+(.*)$|$)")
+
+BLOCK_BEGIN_REGEXP = re.compile(r"(?i)^(\s*)#\+BEGIN_(\w+)(.*)")
+BLOCK_END_REGEXP = re.compile(r"(?i)^(\s*)#\+END_(\w+)")
+BLOCK_RESULT_REGEXP = re.compile(r"(?i)^(\s*)#\+RESULTS:")
+BLOCK_RESULT_CONTENT_REGEXP = re.compile(r"(?:^|\s+):(\s+(.*)|$)")
+
+TABLE_SEP_REGEXP = re.compile(r"^(\s*)(\|[+-|]*)\s*$")
+TABLE_ROW_REGEXP = re.compile(r"^(\s*)(\|.*)")
+TABLE_ALIGN_REGEXP = re.compile(r"^<(l|c|r)>$")
+
+LIST_DESCRIPTIVE_REGEXP = re.compile(r"^(\s*)([+*-])\s+(.*)::(\s|$)")
+LIST_UNORDER_REGEXP = re.compile(r"^(\s*)([+*-])(\s+(.*)|$)")
+LIST_ORDER_REGEXP = re.compile(r"^(\s*)(([0-9]+|[a-zA-Z])[.)])(\s+(.*)|$)")
+LIST_STATUS_REGEXP = re.compile(r"\[( |X|-)\]\s")
+LIST_LEVEL_REGEXP = re.compile(r"(\s*)(.+)$")
+
+HEADLINE_REGEXP = re.compile(
+ r"^(\*+)(?:\s+(.+?))?(?:\s+\[#(.+)\])?(\s+.*?)(?:\s+:(.+):)?$")
+KEYWORD_REGEXP = re.compile(r"^(\s*)#\+([^:]+):(\s+(.*)|$)")
+COMMENT_REGEXP = re.compile(r"^(\s*)#(.*)")
+ATTRIBUTE_REGEXP = re.compile(r"(?:^|\s+)(:[-\w]+)\s+(.*)$")
+
+TODO_KEYWORDS = ("DONE", "TODO")
+
+
+def string_split(s, sep):
+ if not s:
+ return []
+ return s.split(sep)
+
+
+class Parser(object):
+ def __init__(self, content=""):
+ self.lines = content.splitlines()
+ self.level = 0
+ self.element = ""
+ self.children = []
+ self.escape = True
+ self.needparse = True
+ self.parsed_nodes = (
+ "blankline",
+ "headline",
+ "table",
+ "list",
+ "drawer",
+ "block",
+ "block_result",
+ "keyword",
+ "hr",
+ )
+
+ def first_child(self):
+ if len(self.children) == 0:
+ return
+ return self.children[0]
+
+ def last_child(self):
+ if len(self.children) == 0:
+ return
+ return self.children[-1]
+
+ def add_child(self, node):
+ last = self.last_child()
+ if self.is_headline(last):
+ if self.is_properties(node):
+ last.properties = node
+ return
+
+ if not self.is_headline(node):
+ last.add_child(node)
+ return
+
+ if self.is_headline(node) and node.stars > last.stars:
+ last.add_child(node)
+ return
+
+ if self.is_table(last):
+ if self.is_table(node):
+ last.add_child(node)
+ return
+
+ if self.is_list(last):
+ if self.is_blankline(node):
+ last.add_child(node)
+ return
+
+ if node.level > last.level:
+ last.add_child(node)
+ return
+
+ if self.is_list(node) and node.level == last.level:
+ last.add_child(node)
+ return
+
+ if self.is_keyword(last):
+ if self.is_table(node):
+ node.keyword = last
+
+ if self.is_paragraph(last):
+ if self.is_inlinetext(node):
+ last.add_child(node)
+ return
+
+ if self.is_inlinetext(node):
+ self.children.append(self.paragraph(node))
+ return
+
+ self.children.append(node)
+
+ def is_keyword(self, child):
+ return child and isinstance(child, Keyword)
+
+ def is_headline(self, child):
+ return child and isinstance(child, Headline)
+
+ def is_list(self, child):
+ return child and isinstance(child, List)
+
+ def is_table(self, child):
+ return child and isinstance(child, Table)
+
+ def is_src(self, child):
+ return child and isinstance(child, (Src, Example))
+
+ def is_inlinetext(self, child):
+ return child and isinstance(child, InlineText)
+
+ def is_blankline(self, child):
+ return child and isinstance(child, Blankline)
+
+ def is_paragraph(self, child):
+ return child and isinstance(child, Paragraph)
+
+ def is_properties(self, child):
+ return child and isinstance(child, Properties)
+
+ def inlinetext(self, text):
+ return InlineText(text, self.needparse, self.escape)
+
+ def paragraph(self, node):
+ n = Paragraph()
+ n.add_child(node)
+ return n
+
+ def _parse_paired(self, cls, index, lines):
+ node = cls.match(lines[index])
+ if not node:
+ return None, index
+
+ end = len(lines)
+ num = index + 1
+ while num < end:
+ if node.matchend(num, lines):
+ node.preparse(lines[index + 1:num])
+ return node, num
+ num += 1
+ return None, index
+
+ def _parse_nopaired(self, cls, index, lines):
+ node = cls.match(lines[index])
+ if not node:
+ return None, index
+
+ end = len(lines)
+ num = index + 1
+ while num < end:
+ if node.matchend(num, lines):
+ break
+ num += 1
+ node.preparse(lines[index + 1:num])
+ return node, num
+
+ def parse_headline(self, index, lines):
+ return Headline.match(lines[index]), index
+
+ def parse_list(self, index, lines):
+ return List.match(lines[index]), index
+
+ def parse_table(self, index, lines):
+ return self._parse_nopaired(Table, index, lines)
+
+ def parse_drawer(self, index, lines):
+ return self._parse_paired(Drawer, index, lines)
+
+ def parse_block(self, index, lines):
+ return self._parse_paired(Block, index, lines)
+
+ def parse_block_result(self, index, lines):
+ return self._parse_paired(BlockResult, index, lines)
+
+ def parse_blankline(self, index, lines):
+ return Blankline.match(lines[index]), index
+
+ def parse_keyword(self, index, lines):
+ return Keyword.match(lines[index]), index
+
+ def parse_hr(self, index, lines):
+ return Hr.match(lines[index]), index
+
+ def parse_inlinetext(self, index, lines):
+ return self.inlinetext(lines[index]), index
+
+ def parse(self, index, lines):
+ for b in self.parsed_nodes:
+ func = "parse_" + b
+ if not hasattr(self, func):
+ continue
+ block, num = getattr(self, func)(index, lines)
+ if not block:
+ continue
+ return block, num
+
+ return self.parse_inlinetext(index, lines)
+
+ def preparse(self, lines):
+ index = 0
+ while index < len(lines):
+ line = lines[index]
+ node, index = self.parse(index, lines)
+ if node:
+ node.level = len(line) - len(line.strip())
+ self.add_child(node)
+ index += 1
+
+ def to_html(self):
+ if len(self.children) == 0 and len(self.lines) > 0:
+ self.preparse(self.lines)
+
+ children = []
+ for child in self.children:
+ content = child.to_html()
+ if not content:
+ continue
+ children.append(content)
+ text = "\n".join(children)
+ if self.element:
+ return self.element.format(text)
+ return text
+
+ def __str__(self):
+ str_children = [str(child) for child in self.children]
+ return self.__class__.__name__ + '(' + ','.join(str_children) + ')'
+
+ def __repr__(self):
+ return self.__str__()
+
+
+class Headline(Parser):
+ def __init__(
+ self,
+ title,
+ stars=1,
+ keyword=None,
+ priority=None,
+ tags=[],
+ todo_keywords=TODO_KEYWORDS):
+ super(Headline, self).__init__()
+ self.title = title
+ self.stars = stars
+ self.keyword = keyword
+ self.priority = priority
+ self.tags = tags
+ self.properties = None
+ self.todo_keywords = todo_keywords
+
+ @classmethod
+ def match(cls, line):
+ match = HEADLINE_REGEXP.match(line)
+ if not match:
+ return
+
+ stars = len(match[1])
+ keyword = match[2] or ""
+ priority = match[3] or ""
+
+ if keyword and not priority:
+ if len(keyword) >= 4 and keyword[0:2] == "[#":
+ priority = keyword[2:-1]
+ keyword = ""
+
+ title = keyword + match[4]
+ keyword = ""
+
+ return cls(
+ title,
+ stars,
+ keyword,
+ priority,
+ string_split(match[5], ":"),
+ )
+
+ def id(self):
+ hid = 'org-{0}'.format(sha1(self.title.encode()).hexdigest()[:10])
+ if self.properties:
+ return self.properties.get("CUSTOM_ID", hid)
+ return hid
+
+ def toc(self):
+ b = ""
+ if self.keyword:
+ b = b + "{0}".format(self.keyword)
+ if self.priority:
+ b = b + "{0}".format(self.priority)
+
+ b = b + self.inlinetext(self.title).to_html()
+
+ for tag in self.tags:
+ b = b + "{0}".format(tag)
+ return b.strip()
+
+ def to_html(self):
+ b = "
\n{0}\n
" + + def add_child(self, node): + self.children.append(node) + + def to_html(self): + children = [child.to_html() for child in self.children] + return self.element.format("\n{0}\n" + + +class Export(Block): + def __init__(self, language="", params=""): + super(Export, self).__init__("export", params) + self.language = language + self.escape = self.language.upper() != "HTML" + self.parsed_nodes = () + + def to_html(self): + if not self.escape: + return super(Export, self).to_html() + return "" + + +class Src(Block): + def __init__(self, language="", params="", highlight=False): + super(Src, self).__init__("src", params) + self.language = language + self.highlight_code = highlight + self.element = "
\n{1}\n" + self.needparse = False + self.escape = False + self.parsed_nodes = () + + def add_child(self, node): + self.children.append(node) + + def highlight(self, language, text): + return src_highlight(language, text) + + def to_html(self): + text = "\n".join([child.to_html() for child in self.children]) + if self.highlight_code: + return self.highlight(self.language, dedent(text)) + if not self.language: + return "
\n{0}\n".format(dedent(text)) + return self.element.format(self.language, dedent(text)) + + +class Example(Src): + def __init__(self, params="", highlight=False): + super(Example, self).__init__("example", params, highlight) + self.name = "example" + + +class BlockResult(Parser): + def __init__(self): + super(BlockResult, self).__init__() + self.element = "
\n{0}\n" + + @classmethod + def match(cls, line): + match = BLOCK_RESULT_REGEXP.match(line) + if not match: + return + return cls() + + def matchend(self, index, lines): + return not BLOCK_RESULT_CONTENT_REGEXP.match(lines[index]) + + def parse(self, index, lines): + match = BLOCK_RESULT_CONTENT_REGEXP.match(lines[index]) + return self.inlinetext(match[2]), index + + +class ListItem(Parser): + def __init__(self, status=None, checkbox="HTML"): + super(ListItem, self).__init__() + self.status = status + self.checkbox = checkbox + self.element = "
\n{0}\n
" + self.parsed_nodes = () + + def add_child(self, node): + self.children.append(node) + + +class Section(Parser): + def __init__(self, headline): + super(Section, self).__init__() + self.headline = headline + + @property + def stars(self): + return self.headline.stars + + def add_child(self, node): + last = self.last_child() + if not last: + self.children.append(node) + return + + if node.stars > last.stars: + last.add_child(node) + return + self.children.append(node) + + def to_html(self): + text = "{0}
"
+
+ @classmethod
+ def match(cls, line, index):
+ return match_emphasis(cls, CODE_REGEXP, line, index)
+
+
+class Italic(InlineParser):
+ def __init__(self, content):
+ super(Italic, self).__init__(content)
+ self.element = "{0}"
+
+ @classmethod
+ def match(cls, line, index):
+ return match_emphasis(cls, ITALIC_REGEXP, line, index)
+
+
+class Delete(InlineParser):
+ def __init__(self, content):
+ super(Delete, self).__init__(content)
+ self.element = "{0}
"
+
+ @classmethod
+ def match(cls, line, index):
+ return match_emphasis(cls, VERBATIM_REGEXP, line, index)
+
+
+class Underline(InlineParser):
+ def __init__(self, content):
+ super(Underline, self).__init__(content)
+ self.element = "{0}"
+
+ @classmethod
+ def match(cls, line, index):
+ return match_emphasis(cls, UNDERLINE_REGEXP, line, index)
+
+
+class Percent(InlineParser):
+ def __init__(self, content):
+ super(Percent, self).__init__(content)
+ self.element = "[{0}]
"
+
+ @classmethod
+ def match(cls, line, index):
+ match = PERCENT_REGEXP.match(line, index)
+ if not match:
+ return None, index
+ return cls(match[1]), match.end()
+
+
+class Link(InlineParser):
+ def __init__(self, url, desc=None):
+ super(Link, self).__init__(url)
+ self.desc = desc
+
+ @classmethod
+ def match(cls, line, index):
+ match = LINK_REGEXP.match(line, index)
+ if not match:
+ return None, index
+ return cls(match[1], match[2]), match.end()
+
+ def is_img(self):
+ _, ext = os.path.splitext(self.content)
+ return not self.desc and IMG_REGEXP.match(ext)
+
+ def is_vedio(self):
+ _, ext = os.path.splitext(self.content)
+ return not self.desc and VIDEO_REGEXP.match(ext)
+
+ def to_html(self):
+ if self.is_img():
+ return "".format(self.content)
+ if self.is_vedio():
+ return "".format(self.content)
+ if self.desc:
+ return '{1}'.format(self.content, self.desc)
+ return '{1}'.format(self.content, self.content)
+
+
+class Fn(InlineParser):
+ def __init__(self, content):
+ super(Fn, self).__init__(content)
+ self.element = '{0}'
+
+ @classmethod
+ def match(cls, line, index):
+ match = FN_REGEXP.match(line, index)
+ if not match:
+ return None, index
+ return cls(match[3]), match.end()
+
+ def to_html(self):
+ return self.element.format(self.content)
+
+
+class Timestamp(InlineParser):
+ def __init__(self, date="", time="", interval=None):
+ super(Timestamp, self).__init__()
+ self.date = date
+ self.time = time
+ self.interval = interval
+
+ @classmethod
+ def match(cls, line, index):
+ match = TIMESTAMP_REGEXP.match(line, index)
+ if not match:
+ return None, index
+ return cls(match[1], match[3], match[4]), match.end()
+
+
+class Blankline(InlineParser):
+ def __init__(self):
+ super(Blankline, self).__init__()
+
+ @classmethod
+ def match(cls, line):
+ match = BLANKLINE_REGEXP.match(line)
+ if not match:
+ return
+ return cls()
+
+ def to_html(self):
+ return ""
+
+
+class Hr(InlineParser):
+ def __init__(self):
+ super(Hr, self).__init__()
+
+ @classmethod
+ def match(cls, line):
+ if HR_REGEXP.match(line):
+ return cls()
+ return
+
+ def to_html(self):
+ return ""
+
+
+class InlineText(InlineParser):
+ def __init__(self, content="", needparse=True, escape=True):
+ super(InlineText, self).__init__(content)
+ self.needparse = needparse
+ self.escape = escape
+
+ def to_html(self):
+ if self.escape:
+ self.content = html_escape(self.content)
+ if not self.needparse:
+ return self.content
+ return super(InlineText, self).to_html()
diff --git a/app/orgpython/src.py b/app/orgpython/src.py
new file mode 100644
index 0000000..c395d92
--- /dev/null
+++ b/app/orgpython/src.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# ********************************************************************************
+# Copyright © 2017-2020 jianglin
+# File Name: src.py
+# Author: jianglin
+# Email: mail@honmaple.com
+# Created: 2018-02-26 12:41:22 (CST)
+# Last Update: Sunday 2020-08-16 19:45:32 (CST)
+# By:
+# Description:
+# ********************************************************************************
+try:
+ import pygments
+ from pygments import lexers
+ from pygments import formatters
+except ImportError:
+ pygments = None
+
+
+def highlight(language, text):
+ if pygments is None:
+ return text
+
+ try:
+ lexer = lexers.get_lexer_by_name(language)
+ except pygments.util.ClassNotFound:
+ lexer = lexers.guess_lexer(text)
+ formatter = formatters.HtmlFormatter()
+ return pygments.highlight(text, lexer, formatter)
diff --git a/tasks.py b/tasks.py
index 0774bcc..a003f63 100644
--- a/tasks.py
+++ b/tasks.py
@@ -6,10 +6,19 @@ import asyncio
from pathlib import Path
from prompt_toolkit import prompt
+from prompt_toolkit.key_binding import KeyBindings
from invoke import Context # type: ignore
from invoke import run # type: ignore
from invoke import task # type: ignore
+_kb = KeyBindings()
+
+
+@_kb.add("c-@")
+def _(event):
+ """Save multi-line buffer on CTRL + space"""
+ event.current_buffer.validate_and_handle()
+
@task
def config(ctx):
from Crypto.PublicKey import RSA
@@ -92,7 +101,6 @@ def accept_follow(ctx):
async def _do():
async with async_session() as db_session: #type: ignore
try:
-
exist_request = (
await db_session.scalars(
select(models.IncomingActivity).where(
@@ -127,5 +135,36 @@ def accept_follow(ctx):
print("Done!")
-
asyncio.run(_do())
+
+
+@task
+def send_note(ctx):
+ from app.database import async_session
+ from app.activitypub import VisibilityEnum
+ from app.boxes import _send_create
+ from app.orgpython import to_html
+
+
+ content = prompt(
+ (
+ "note contents, in org mode, "
+ "use [CTRL] + [SPACE] to submit:\n"
+ ),
+ key_bindings=_kb,
+ multiline=True,
+ )
+ content = to_html(content)
+
+ async def _dodo():
+ async with async_session() as db_session: #type: ignore
+
+ await _send_create(
+ db_session,
+ "Note",
+ content,
+ VisibilityEnum.PUBLIC
+ )
+ print("Done!")
+
+ asyncio.run(_dodo())