Commit Diff


commit - eea70320c3f4594d8d2a0454dd95fe3277ad8f9e
commit + 6253df7fd690eb69235c862b14329b396a4b9eae
blob - /dev/null
blob + 65779c1903b343d11ea7f8e40bdf4c4379061414 (mode 644)
--- /dev/null
+++ feeds.gmi
@@ -0,0 +1,51 @@
+# Катастеризм. Подписки
+
+Ниже приведены ссылки, на которые подписан проект "Катастеризм".
+
+Критерии коллекции:
+* русскоязычные ссылки
+* содержимое соответствует формату "Subscribing to Gemini pages"
+
+=> gemini://geminiprotocol.net/docs/companion/subscription.gmi Subscribing to Gemini pages
+
+Если вы хотите изменить этот список, пожалуйста, напишите письмо на электронную почту
+```
+katasterismos@to.any-key.press
+```
+
+## Коллекция подписок
+
+=> gemini://academia.fzrw.info/ru/blog
+=> gemini://academia.fzrw.info/ru/encyclopedia
+=> gemini://academia.fzrw.info/ru/publications
+=> gemini://alexey.shpakovsky.ru/rulog/
+=> gemini://any-key.press/vgi/atom2gemfeed/?gemini%3A%2F%2Fany-key.press%2Fatom.xml
+=> gemini://armitage.flounder.online/gemlog/
+=> gemini://any-key.press/vgi/atom2gemfeed/?gemini%3A%2F%2Fbasnja.ru%2Fatom.xml
+=> gemini://bbs.geminispace.org/s/russian?feed
+=> gemini://byzoni.org/gemlog.gmi
+=> gemini://causa-arcana.com/ru/blog/feed.gmi
+=> gemini://gemini.quietplace.xyz/~razzlom/gemlog/
+=> gemini://gemlog.blue/users/3550/
+=> gemini://gemlog.blue/users/DaVINCIs23/
+=> gemini://gemlog.blue/users/KindFoxie/
+=> gemini://gemlog.blue/users/abrbus/
+=> gemini://gemlog.blue/users/antcating/
+=> gemini://gemlog.blue/users/cu8wllwp/
+=> gemini://gemlog.blue/users/freedom/
+=> gemini://gemlog.blue/users/musu_pilseta/
+=> gemini://gemlog.stargrave.org/
+=> gemini://hugeping.ru/
+=> gemini://any-key.press/vgi/atom2gemfeed/?gemini%3A%2F%2Fhugeping.ru%2Fmicro%2Fatom.xml
+=> gemini://karabas.flounder.online/gemlog
+=> gemini://kotobank.ch/~merlin/feed_ru.gmi
+=> gemini://muu-online.ru/
+=> gemini://parthen.smol.pub/
+=> gemini://polyserv.xyz/
+=> gemini://pub.phreedom.club/~kornilovnet/glog/
+=> gemini://pub.phreedom.club/~tolstoevsky/glog/
+=> gemini://sn4il.site/
+=> gemini://spline-online.ru/
+=> gemini://sysrq.in/ru/gemlog/
+=> gemini://tilde.team/~runation/Post/post.gmi
+=> gemini://topotun.dynu.com/blog/
blob - /dev/null
blob + dea07540fce25f229f5ad5957e99bf8d14295dc5 (mode 644)
--- /dev/null
+++ katasterismos.py
@@ -0,0 +1,90 @@
+from email.message import Message
+from pathlib import Path
+from socket import create_connection, gaierror
+from ssl import SSLContext, SSLError, PROTOCOL_TLS_CLIENT, CERT_NONE
+from urllib.parse import urlparse, urljoin, uses_relative, uses_netloc
+
+# for urljoin:
+uses_relative.append("gemini")
+uses_netloc.append("gemini")
+
+
+class FeedError(RuntimeError):
+    def __init__(self, url, message):
+        super().__init__(url, message)
+
+
+def get(url):
+    for _ in range(6): # first request + 5 consecutive redirects
+        parsed = urlparse(url)
+        if parsed.scheme.lower() != "gemini":
+            raise FeedError(url=url, message="Not gemini://")
+
+        context = SSLContext(PROTOCOL_TLS_CLIENT)
+        context.check_hostname = False
+        context.verify_mode = CERT_NONE
+        try:
+            with create_connection((parsed.hostname, parsed.port or 1965)) as raw_s:
+                with context.wrap_socket(raw_s, server_hostname=parsed.hostname) as s:
+                    s.sendall((url + '\r\n').encode("UTF-8"))
+                    fp = s.makefile("rb")
+                    splitted = fp.readline().decode("UTF-8").strip().split(maxsplit=1)
+                    status = splitted[0]
+                    if status.startswith("3") and len(splitted) == 2:
+                        # redirect
+                        url = urljoin(url, splitted[1])
+                        continue
+                    if not status.startswith("2"):
+                        message = f"Unexpected answer: {splitted[0]}"
+                        if len(splitted) == 2:
+                            message += f"({splitted[1]})"
+                        raise FeedError(url=url, message=message)
+                    mime = splitted[1].lower() if len(splitted) == 2 else "text/gemini"
+                    if not mime.startswith("text/gemini"):
+                        raise FeedError(url=url, message=f"Unexpected MIME: {mime!r}")
+
+                    m = Message()
+                    m['content-type'] = mime
+                    return fp.read().decode(m.get_param('charset') or "UTF-8", errors='ignore')
+        except gaierror as error:
+            raise FeedError(url=url, message=str(error))
+        except SSLError as error:
+            raise FeedError(url=url, message=str(error))
+
+
+def parse(url, feed_body):
+    header = None
+    for line in feed_body.splitlines():
+        splitted = line.rstrip().split(maxsplit=1)
+        if len(splitted) == 2 and splitted[0] == "#" and not header:
+            header = splitted[1]
+    print(f"{header} ({url})" if header else url)
+
+
+def daily(feeds_gmi):
+    feeds = {}
+    errors = []
+
+    header_2_passes = False
+    for line in feeds_gmi.read_text(encoding="utf8").splitlines():
+        splitted = line.rstrip().split()
+        if splitted and splitted[0] == "##":
+            header_2_passes = True
+        elif header_2_passes and len(splitted) > 1 and splitted[0] == "=>":
+            try:
+                url = splitted[1]
+                parse(url, get(url))
+                feeds[url] = []
+            except FeedError as error:
+                errors.append(error)
+
+    raise NotImplementedError(len(feeds), feeds, len(errors), errors)
+
+
+if __name__ == '__main__':
+    from argparse import ArgumentParser
+    parser = ArgumentParser()
+    parser.add_argument("--feeds", type=Path, default="feeds.gmi", help="path to feeds.gmi file")
+    parser.add_argument("action", choices=["daily"])
+    args = parser.parse_args()
+    daily(args.feeds)