commit 6253df7fd690eb69235c862b14329b396a4b9eae from: Aleksey Ryndin date: Sun Jan 26 09:08:56 2025 UTC Add work-in-progress 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)