about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/rsskey.py89
1 files changed, 89 insertions, 0 deletions
diff --git a/src/rsskey.py b/src/rsskey.py
new file mode 100755
index 0000000..f88bd7e
--- /dev/null
+++ b/src/rsskey.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+# RSS feed mirror on Misskey
+# Copyright (C) 2021  Nguyễn Gia Phong
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+from configparser import ConfigParser
+from contextlib import AsyncExitStack
+from functools import partial
+from re import split, sub
+
+from dateutil.parser import parse as parse_time
+from feedparser import parse as parse_feed
+from httpx import AsyncClient
+from loca import Loca
+from markdownify import markdownify as md
+from trio import open_nursery, run
+
+
+async def create(client, **note):
+    """Create the given note and return its ID."""
+    response = await client.post('notes/create', json=note)
+    return response.json()['createdNote']['id']
+
+
+async def post(job, client, link, title, summary):
+    """Post the given entry to Misskey.
+
+    In case the link was already posted, the entry shall be skipped.
+    """
+    search = await client.post('notes/search', json={'query': link,
+                                                     'userId': job['user']})
+    if search.json(): return
+
+    note = partial(create, client, i=job['token'], visibility='home', cw=title)
+    original = f'Original: {link}'
+    rest = '\n\n'.join((*map(partial(sub, r'\s+', ' '),
+                             split(r'\s*\n{2,}\s*', md(summary).strip())),
+                        original))
+    limit = int(job['limit'])
+    parent = None
+
+    while len(rest) > limit:
+        index = rest.rfind('\n\n', 0, limit+2)  # split paragraphs
+        if index < 0:
+            index = rest.rfind('. ', 0, limit+1)  # split sentences
+            if index < 0:
+                parent = await note(text=original, replyId=parent)
+                return
+            first, rest = rest[:index+1], rest[index+2:]
+        else:
+            first, rest = rest[:index], rest[index+2:]
+        parent = await note(text=first, replyId=parent)
+    parent = await note(text=rest, replyId=parent)
+
+
+async def mirror(nursery, job, client):
+    """Perform the given mirror job."""
+    feed = await client.get(job['source'])
+    for entry in parse_feed(feed.text)['entries']:
+        nursery.start_soon(post, job, client, entry['link'],
+                           entry['title'], entry['summary'])
+
+
+async def main():
+    """Parse and run jobs."""
+    config = ConfigParser()
+    config.read(Loca().user.config()/'rsskey'/'jobs.conf')
+    async with AsyncExitStack() as stack, open_nursery() as nursery:
+        for section in config:
+            if section == 'DEFAULT': continue
+            job = config[section]
+            client = AsyncClient(base_url=job['dest'])
+            await stack.enter_async_context(client)
+            nursery.start_soon(mirror, nursery, job, client)
+
+
+if __name__ == '__main__': run(main)