Commit Diff


commit - a58fea8282cec6d0d2c2301d91ae5dd72c46a8f1
commit + 86729cc95e7f5ff6c502de20b2685f190eee8910
blob - a0990367ef8b03c70c29d285e22ef85907e1d0b7
blob + f6bd4a8274f8b4773299c3a8cb594f0afb7a5773
--- README
+++ README
@@ -1 +1,83 @@
-TBD
+# Агрегатор "Катастеризм" ("Katasterismos")
+
+Агрегатор русскоязычных gemini-публикаций "Катастеризм" ("Katasterismos"), отображающий перекрестные ссылки между публикациями.
+
+
+## Работающий экземпляр агрегатора
+
+Результат ежедневной работы агрегатора доступен по следующему адресу:
+
+=> gemini://pub.phreedom.club/~katasterismos/
+
+На этой странице собраны публикации за последние восемь недель.
+
+
+## Идея агрегатора
+
+Идея была нагло украдена из ветки обсуждений "Реквестирую русскоязычный аналог Cosmos" на GSR:
+=> gemini://geminispace.ru/s/74/ Реквестирую русскоязычный аналог Cosmos - агрегатор, который составляет треды из ссылающихся на кого-то ещё посты.
+
+
+## Исходный код
+
+Код агрегатора написан на Python3 и доступен в git-репозитории:
+```
+ssh://anonymous@got.any-key.press/katasterismos
+```
+=> https://got.any-key.press/?action=summary&path=katasterismos.git Web интерфейс к git репозиторию
+
+=> gemini://any-key.press/tt/welcome.gmi Сделано в соответствии с духом Тривиальных Технологий
+
+
+## Ленты публикаций агрегатора
+
+Ниже приведены ссылки, на которые подписан проект "Катастеризм".
+
+Критерии коллекции:
+* русскоязычные содержимое (хотя бы частично)
+* содержимое соответствует формату "Subscribing to Gemini pages"
+
+=> gemini://geminiprotocol.net/docs/companion/subscription.gmi Subscribing to Gemini pages
+
+Вы всегда можете помочь актуализировать этот список: напишите письмо на электронную почту
+```
+katasterismos@to.any-key.press
+```
+=> mailto:katasterismos@to.any-key.press
+
+### [FEEDS] Ленты публикаций, за которыми следит агрегатор
+
+=> 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 - 65779c1903b343d11ea7f8e40bdf4c4379061414 (mode 644)
blob + /dev/null
--- feeds.gmi
+++ /dev/null
@@ -1,51 +0,0 @@
-# Катастеризм. Подписки
-
-Ниже приведены ссылки, на которые подписан проект "Катастеризм".
-
-Критерии коллекции:
-* русскоязычные ссылки
-* содержимое соответствует формату "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 + 51cf5e91dd283ac2b99f2a20bf51e149c488eb70 (mode 644)
--- /dev/null
+++ index.gmi
@@ -0,0 +1,77 @@
+# Агрегатор "Катастеризм" ("Katasterismos")
+
+Агрегатор русскоязычных gemini-публикаций "Катастеризм" ("Katasterismos"), отображающий перекрестные ссылки между публикациями.
+
+## Работающий экземпляр агрегатора
+
+Результат ежедневной работы агрегатора доступен по следующему адресу:
+
+=> gemini://pub.phreedom.club/~katasterismos/
+
+## Идея агрегатора
+
+Идея была нагло украдена из ветки обсуждений "Реквестирую русскоязычный аналог Cosmos" на GSR:
+=> gemini://geminispace.ru/s/74/ Реквестирую русскоязычный аналог Cosmos - агрегатор, который составляет треды из ссылающихся на кого-то ещё посты.
+
+## Исходный код
+
+Код агрегатора написан на Python3 и доступен в git-репозитории:
+```
+ssh://anonymous@got.any-key.press/katasterismos
+```
+=> https://got.any-key.press/?action=summary&path=katasterismos.git Web интерфейс к git репозиторию
+
+=> gemini://any-key.press/tt/welcome.gmi Сделано в соответствии с духом Тривиальных Технологий
+
+## Ленты публикаций агрегатора
+
+Ниже приведены ссылки, на которые подписан проект "Катастеризм".
+
+Критерии коллекции:
+* русскоязычные содержимое (хотя бы частично)
+* содержимое соответствует формату "Subscribing to Gemini pages"
+
+=> gemini://geminiprotocol.net/docs/companion/subscription.gmi Subscribing to Gemini pages
+
+Вы всегда можете помочь актуализировать этот список: напишите письмо на электронную почту
+```
+katasterismos@to.any-key.press
+```
+=> mailto:katasterismos@to.any-key.press
+
+### [LINKS] Ленты публикаций, за которыми следит агрегатор
+
+=> 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 - bb0516d44da55d09e45a46b4c3c12e9694433c14
blob + 01e0fa00f9401e52306626d36d62acf90487bd63
--- katasterismos.py
+++ katasterismos.py
@@ -1,5 +1,5 @@
 from dataclasses import dataclass, field
-from datetime import date
+from datetime import date, datetime, timedelta
 from email.message import Message
 from pathlib import Path
 from socket import create_connection, gaierror
@@ -40,7 +40,7 @@ def get(url):
         context.check_hostname = False
         context.verify_mode = CERT_NONE
         try:
-            with create_connection((parsed.hostname, parsed.port or 1965)) as raw_s:
+            with create_connection((parsed.hostname, parsed.port or 1965), timeout=15.) 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")
@@ -53,7 +53,7 @@ def get(url):
                     if not status.startswith("2"):
                         message = f"Unexpected answer: {splitted[0]}"
                         if len(splitted) == 2:
-                            message += f"({splitted[1]})"
+                            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"):
@@ -62,15 +62,15 @@ def get(url):
                     m = Message()
                     m['content-type'] = mime
                     return fp.read().decode(m.get_param('charset') or "UTF-8", errors='ignore')
-        except gaierror as error:
+        except OSError as error:
             raise FeedError(url=url, message=str(error))
         except SSLError as error:
             raise FeedError(url=url, message=str(error))
 
 
-def parse_feed(url, feed_body, feeds):
+def parse_feed(url, body, feeds):
     _feed = Feed(url=url)
-    for line in feed_body.splitlines():
+    for line in body.splitlines():
         if line.startswith("#") and line[1:2] != "#" and not _feed.title:
             _feed.title = line[1:].strip()
         elif line.startswith("=>"):
@@ -90,23 +90,76 @@ def parse_feed(url, feed_body, feeds):
                     feed=_feed
                 )
 
-def daily(feeds_gmi):
+
+def iterate_links(body):
+    for line in body.splitlines():
+        if line.startswith("=>"):
+            splitted = line[2:].strip().split(maxsplit=1)
+            if splitted:
+                yield splitted[0]
+
+
+def daily(index_gmi):
     feeds = {}
     errors = []
 
     header_2_passes = False
-    for line in feeds_gmi.read_text(encoding="utf8").splitlines():
-        if line.startswith("##"):
+    for line in index_gmi.read_text(encoding="utf8").splitlines():
+        if line.startswith("### [FEEDS]"):
             header_2_passes = True
         elif header_2_passes and line.startswith("=>"):
             try:
                 url = line[2:].strip()
                 parse_feed(url, get(url), feeds)
-                raise NotImplementedError(len(feeds), feeds, len(errors), errors)
             except FeedError as error:
                 errors.append(error)
 
+    dt_now = datetime.now()
+    date_limit = (dt_now - timedelta(weeks=8)).date()
+    rv = [
+        f"# Katasterismos ({date_limit.isoformat()} - {dt_now.date().isoformat()})",
+        "",
+        'Агрегатор русскоязычных gemini-публикаций "Катастеризм" ("Katasterismos"), отображающий перекрестные ссылки между публикациями.',
+        "",
+        "=> about.gmi Подробности",
+    ]
 
+    current_date = None
+    for entry in reversed(
+        sorted(
+            [entry for url, entry in feeds.items() if entry.updated >= date_limit],
+            key=lambda entry: entry.updated,
+        )
+    ):
+        if current_date != entry.updated:
+            current_date = entry.updated
+            rv.append("")
+            rv.append("")
+            rv.append(f"## {current_date.isoformat()}")
+
+        rv.append("")
+        rv.append(f"=> {entry.url} {entry.feed.title} {entry.updated.isoformat()} {entry.title}")
+        for link in iterate_links(get(entry.url)):
+            url = urljoin(entry.url, link)
+            linked_to = feeds.get(url)
+            if linked_to:
+                rv.append(f"=> {url} ↳ {linked_to.feed.title} {linked_to.updated.isoformat()} {linked_to.title}")
+
+
+    if errors:
+        rv.append("")
+        rv.append("")
+        rv.append("## Errors")
+        for error in errors:
+            rv.append("")
+            rv.append(f"=> {error.args[0]}")
+            rv.append("```")
+            rv.append(error.args[1])
+            rv.append("```")
+
+    print("\r\n".join(rv))
+
+
 if __name__ == '__main__':
     from argparse import ArgumentParser
     parser = ArgumentParser()