commit - eea70320c3f4594d8d2a0454dd95fe3277ad8f9e
commit + 6253df7fd690eb69235c862b14329b396a4b9eae
blob - /dev/null
blob + 65779c1903b343d11ea7f8e40bdf4c4379061414 (mode 644)
--- /dev/null
+++ feeds.gmi
+# Катастеризм. Подписки
+
+Ниже приведены ссылки, на которые подписан проект "Катастеризм".
+
+Критерии коллекции:
+* русскоязычные ссылки
+* содержимое соответствует формату "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
+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)