From 373b7b1f05d3f3c3b5cd4f3a17589db25a0bca2d Mon Sep 17 00:00:00 2001 From: "Schimon Jehudah, Adv." Date: Wed, 30 Oct 2024 22:04:05 +0200 Subject: [PATCH] CSS : Various of modifications and several fixes; Python : Improve caching system; Python : Support XEP-0292: vCard4 Over XMPP (Thank you. Marvin W); SVG : Add new icons, including characters as temporary placeholders (Thank you. Tigase); TOML : Add more systems and modify properties of clients; XHTML : Improve design and add a new page of extended vcard. --- clients.toml | 463 +++++++++++++++----------- css/stylesheet.css | 88 ++++- fasi.py | 688 +++++++++++++++++++++++++++++++-------- img/atalk.svg | 351 ++++++++++++++++++++ img/beagle.svg | 1 + img/bruno.svg | 1 + img/bsd.svg | 109 +++++++ img/candy.svg | 1 + img/chat-o-matic.svg | 1 + img/convo.svg | 43 +++ img/coyim.svg | 1 + img/dergchat.svg | 295 +++++++++++++++++ img/leechcraft.svg | 724 +++++++++++++++++++++++++++++++++++++++++ img/mcabber.svg | 1 + img/profanity_logo.svg | 237 ++++++++++++++ img/siskin.svg | 136 ++++++++ img/spark.svg | 1 + img/xmpp-web.svg | 1 + systems.toml | 1 + xep_0060/README | 1 + xhtml/download.xhtml | 25 +- xhtml/jid.xhtml | 40 ++- xhtml/node.xhtml | 8 +- xhtml/vcard.xhtml | 135 ++++++++ 24 files changed, 2980 insertions(+), 372 deletions(-) create mode 100644 img/atalk.svg create mode 100644 img/beagle.svg create mode 100644 img/bruno.svg create mode 100644 img/bsd.svg create mode 100644 img/candy.svg create mode 100644 img/chat-o-matic.svg create mode 100644 img/convo.svg create mode 100644 img/coyim.svg create mode 100644 img/dergchat.svg create mode 100644 img/leechcraft.svg create mode 100644 img/mcabber.svg create mode 100644 img/profanity_logo.svg create mode 100644 img/siskin.svg create mode 100644 img/spark.svg create mode 100644 img/xmpp-web.svg create mode 100644 xep_0060/README create mode 100644 xhtml/vcard.xhtml diff --git a/clients.toml b/clients.toml index 0744774..4fb65c9 100644 --- a/clients.toml +++ b/clients.toml @@ -15,43 +15,46 @@ title = "Aparté" about = """ Simple XMPP console client written in Rust and inspired by Profanity. + +It supports OMEMO and can display images thanks to sixel. """ posix = "https://github.com/paulfariello/aparte/releases" -properties = ["chat", "console", "desktop", "featured"] +properties = ["chat", "console", "desktop", "featured", "omemo"] resources = [ + { url = "xmpp:aparte@conference.fariello.eu?join", txt = "Support group chat" }, { url = "https://github.com/paulfariello/aparte", txt = "Project repository" }, ] -#[atalk] -#title = "aTalk" -#about = """ -#XMPP/Jabber client with encrypted instant messaging and video calls. -## -#An encrypted instant messaging with video call and GPS features for Divest OS. -#""" -#android = "https://f-droid.org/packages/org.atalk.android/" -#properties = ["chat", "fdroid", "graphical", "mobile", "omemo", "otr", "zrtp"] -#resources = [ -# { url = "https://f-droid.org/en/packages/org.atalk.android", txt = "F-Droid package" }, -# { url = "https://atalk.sytes.net/atalk/", txt = "Project homepage" }, -#] +[atalk] +title = "aTalk" +about = """ +XMPP/Jabber client with encrypted instant messaging and video calls. +# +An encrypted instant messaging with video call and GPS features for Divest OS. +""" +android = "https://f-droid.org/packages/org.atalk.android/" +properties = ["chat", "fdroid", "graphical", "mobile", "omemo", "otr", "zrtp"] +resources = [ + { url = "https://f-droid.org/en/packages/org.atalk.android", txt = "F-Droid package" }, + { url = "https://atalk.sytes.net/atalk/", txt = "Project homepage" }, +] -#[beagle] -#title = "Beagle" -#about = """ -#Beagle IM by Tigase, Inc. is a lightweight and powerful XMPP client for macOS. -# -#It provides an easy way to start using XMPP protocol (formelly known as \ -#Jabber) if you've never used it before. -# -#Veterans of the protocol will find many features with which they are familiar \ -#and a few enhancements. -#""" -#apple = "https://beagle.im/#about" -#properties = ["chat", "desktop", "graphical", "omemo"] -#resources = [ -# { url = "https://beagle.im", txt = "Project homepage" }, -#] +[beagle] +title = "Beagle" +about = """ +Beagle IM by Tigase, Inc. is a lightweight and powerful XMPP client for macOS. + +It provides an easy way to start using XMPP protocol (formelly known as \ +Jabber) if you've never used it before. + +Veterans of the protocol will find many features with which they are familiar \ +and a few enhancements. +""" +apple = "https://beagle.im/#about" +properties = ["chat", "desktop", "graphical", "omemo"] +resources = [ + { url = "https://beagle.im", txt = "Project homepage" }, +] [blabber] title = "blabber.im" @@ -68,50 +71,50 @@ resources = [ { url = "https://blabber.im", txt = "Project homepage" }, ] -#[bruno] -#title = "Bruno" -#about = """ -#Bruno is the cutest Jabber/XMPP Instant Messaging (IM) app available. It is a \ -#themed version of the open source yaxim app. -# -#You can use Bruno if the other IM apps are just not stylish enough. -#""" -#android = "https://yaxim.org/download/" -#properties = ["chat", "graphical", "mobile"] -#resources = [ -# { url = "https://yaxim.org/bruno/", txt = "Project homepage" }, -#] +[bruno] +title = "Bruno" +about = """ +Bruno is the cutest Jabber/XMPP Instant Messaging (IM) app available. It is a \ +themed version of the open source yaxim app. -#[candy] -#title = "Candy" -#about = """ -#A JavaScript-based multi-user chat client. -# -#There are plenty of HTML-based chat clients out there. Most of them are built \ -#to emulate your instant messenger. They offer you tons of settings. They can \ -#join multiple networks, let you edit your profile, and even manage your \ -#message history. -# -#Candy is different. It is built for your community. -#""" -#browser = "http://candy-chat.github.io/candy/" -#properties = ["chat", "desktop", "graphical", "mobile"] -#resources = [ -# { url = "http://candy-chat.github.io/candy/", txt = "Project homepage" }, -#] +You can use Bruno if the other IM apps are just not stylish enough. +""" +android = "https://yaxim.org/download/" +properties = ["chat", "graphical", "mobile"] +resources = [ + { url = "https://yaxim.org/bruno/", txt = "Project homepage" }, +] -#[chat-o-matic] -#title = "Chat-O-Matic" -#about = """ -#A multi-protocol chat program for Haiku -# -#Protocols natively supported include IRC and XMPP. -#""" -#haiku = "https://github.com/JadedCtrl/Chat-O-Matic/releases" -#properties = ["chat", "desktop", "graphical"] -#resources = [ -# { url = "https://github.com/JadedCtrl/Chat-O-Matic", txt = "Project homepage" }, -#] +[candy] +title = "Candy" +about = """ +A JavaScript-based multi-user chat client. + +There are plenty of HTML-based chat clients out there. Most of them are built \ +to emulate your instant messenger. They offer you tons of settings. They can \ +join multiple networks, let you edit your profile, and even manage your \ +message history. + +Candy is different. It is built for your community. +""" +browser = "http://candy-chat.github.io/candy/" +properties = ["chat", "desktop", "graphical", "mobile"] +resources = [ + { url = "http://candy-chat.github.io/candy/", txt = "Project homepage" }, +] + +[chat-o-matic] +title = "Chat-O-Matic" +about = """ +A multi-protocol chat program for Haiku + +Protocols natively supported include IRC and XMPP. +""" +haiku = "https://github.com/JadedCtrl/Chat-O-Matic/releases" +properties = ["chat", "desktop", "graphical"] +resources = [ + { url = "https://github.com/JadedCtrl/Chat-O-Matic", txt = "Project homepage" }, +] [chatsecure] title = "ChatSecure" @@ -194,33 +197,73 @@ resources = [ { url = "https://conversejs.org", txt = "Project homepage" }, ] -#[coyim] -#title = "CoyIM" -#about = """ -#CoyIM is a standalone chat client for computers that focuses on safety and \ -#security. -# -#It is a self-contained program that is safe from the moment it starts up. -# -#CoyIM only supports one chat protocol - XMPP (sometimes known as Jabber). -# -#When creating CoyIM, we carefully evaluate and pick the features that are \ -#necessary to create a good chat experience, while keeping the attack surface \ -#of the system to a minimum. -# -#At the same time, we want CoyIM to be part of an open ecosystem. You will not \ -#be locked in by using CoyIM. You can talk to people using other XMPP and OTR \ -#clients as well. CoyIM also allows you to use accounts you have already \ -#created with other software. -#""" -#apple = "https://coy.im/#download-section" -#linux = "https://coy.im/#download-section" -#windows = "https://coy.im/#download-section" -#properties = ["chat", "desktop", "graphical", "otr"] -#resources = [ -# { url = "https://github.com/coyim/coyim", txt = "Project repository" }, -# { url = "https://coy.im", txt = "Project homepage" }, -#] +[convo] +title = "Convo" +about = """ +A Jabber/XMPP client for Project Pris / GerdaOS and KaiOS devices. + +Convo is a basic XMPP messaging client for KaiOS which supports sending of \ +messages to existing contacts and joining to existing groupchats. + +The app is a bit limited on its own, but works well along with a companion \ +desktop app. + +Convo is still at the state of "work in progress" and is currently under \ +development. +""" +kaios = "https://git.disroot.org/badrihippo/convo/releases" +properties = ["chat", "featured", "graphical", "mobile"] +resources = [ + { url = "bhackers:Convo", txt = "Install via the BananaHackers store" }, + { url = "https://store.bananahackers.net/#Convo", txt = "BananaHackers store package" }, + { url = "xmpp:convo@chat.disroot.org?join", txt = "Support group chat" }, + { url = "https://git.disroot.org/badrihippo/convo", txt = "Project repository" }, +] + +[coyim] +title = "CoyIM" +about = """ +CoyIM is a standalone chat client for computers that focuses on safety and \ +security. + +It is a self-contained program that is safe from the moment it starts up. + +CoyIM only supports one chat protocol - XMPP (sometimes known as Jabber). + +When creating CoyIM, we carefully evaluate and pick the features that are \ +necessary to create a good chat experience, while keeping the attack surface \ +of the system to a minimum. + +At the same time, we want CoyIM to be part of an open ecosystem. You will not \ +be locked in by using CoyIM. You can talk to people using other XMPP and OTR \ +clients as well. CoyIM also allows you to use accounts you have already \ +created with other software. +""" +apple = "https://coy.im/#download-section" +bsd = "https://www.freshports.org/net-im/coyim/" +linux = "https://coy.im/#download-section" +windows = "https://coy.im/#download-section" +properties = ["chat", "desktop", "graphical", "otr"] +resources = [ + { url = "https://github.com/coyim/coyim", txt = "Project repository" }, + { url = "https://coy.im", txt = "Project homepage" }, +] + +[dergchat] +title = "Dergchat" +about = """ +Dergchat is a small chat client for the XMPP protocol. It is written in Rust, \ +based on xmpp-rs and Dioxus. + +Please note: this app is not really in a usable state. Do not try to use this \ +at the moment. +""" +linux = "https://codeberg.org/Mizah/Dergchat" +properties = ["chat", "desktop", "graphical"] +resources = [ + { url = "xmpp:dergchat@conference.mizah.xyz?join", txt = "Support group chat" }, + { url = "https://codeberg.org/Mizah/Dergchat", txt = "Project repository" }, +] [dino] title = "Dino" @@ -236,6 +279,7 @@ notifications. Dino fetches history from the server and synchronizes messages with other \ services. """ +bsd = "https://www.freshports.org/net-im/dino/" linux = "https://dino.im/#download" properties = ["chat", "desktop", "featured", "graphical", "mobile", "omemo", "openpgp"] resources = [ @@ -244,6 +288,24 @@ resources = [ { url = "https://dino.im", txt = "Project homepage" }, ] +[freetalk] +title = "Freetalk" +about = """ +GNU Freetalk is a console based chat client for Jabber and other XMPP servers. + +It has context sensitive auto-completion for buddy names, commands, and even \ +ordinary English words. Similar to GNU Emacs, + +You can customize and extend GNU Freetalk with Scheme language. +""" +bsd = "https://www.freshports.org/net-im/freetalk/" +posix = "https://www.gnu.org/software/freetalk/" +properties = ["chat", "console", "desktop"] +resources = [ + { url = "https://lists.gnu.org/mailman/listinfo/freetalk-dev", txt = "Mailing list" }, + { url = "https://www.gnu.org/software/freetalk/", txt = "Project homepage" }, +] + [gajim] title = "Gajim" about = """ @@ -256,9 +318,10 @@ Gajim integrates well with your other devices: simply continue conversations \ on your mobile device. """ apple = "https://gajim.org/download/#macos" +bsd = "https://www.freshports.org/net-im/gajim/" linux = "https://gajim.org/download/#linux" windows = "https://gajim.org/download/#windows" -properties = ["adhoc", "admin", "chat", "desktop", "featured", "graphical", "omemo", "openpgp"] +properties = ["adhoc", "admin", "chat", "desktop", "featured", "graphical", "omemo", "openpgp", "plugin"] resources = [ { url = "https://flathub.org/apps/org.gajim.Gajim", txt = "Flathub package" }, { url = "https://apps.microsoft.com/detail/9pggf6hd43f9?hl=en-us&gl=US", txt = "Windows package" }, @@ -266,23 +329,6 @@ resources = [ { url = "https://gajim.org", txt = "Project homepage" }, ] -[freetalk] -title = "GNU Freetalk" -about = """ -GNU Freetalk is a console based chat client for Jabber and other XMPP servers. - -It has context sensitive auto-completion for buddy names, commands, and even \ -ordinary English words. Similar to GNU Emacs, - -You can customize and extend GNU Freetalk with Scheme language. -""" -posix = "https://www.gnu.org/software/freetalk/" -properties = ["chat", "console", "desktop"] -resources = [ - { url = "https://lists.gnu.org/mailman/listinfo/freetalk-dev", txt = "Mailing list" }, - { url = "https://www.gnu.org/software/freetalk/", txt = "Project homepage" }, -] - [irssi] title = "Irssi" about = """ @@ -290,9 +336,10 @@ Irssi is a modular text mode chat client. It comes with IRC support built in. irssi-xmpp is an Irssi plugin to connect to the XMPP network (jabber). """ +bsd = "https://www.freshports.org/irc/irssi-xmpp/" haiku = "https://depot.haiku-os.org/irssi" posix = "https://cybione.org/~irssi-xmpp/" -properties = ["chat", "console", "desktop", "haikudepot"] +properties = ["chat", "console", "desktop", "haikudepot", "plugin"] resources = [ { url = "https://cybione.org/~irssi-xmpp/", txt = "irssi-xmpp project repository" }, { url = "https://irssi.org", txt = "Irssi homepage" }, @@ -306,6 +353,8 @@ An XMPP client for Emacs jabber.el is an XMPP client for Emacs. XMPP (also known as 'Jabber') is an \ IETF-standard federated instant messaging protocol. """ +bsd = "https://codeberg.org/emacs-jabber/emacs-jabber#how-to-install" +haiku = "https://codeberg.org/emacs-jabber/emacs-jabber#how-to-install" posix = "https://codeberg.org/emacs-jabber/emacs-jabber#how-to-install" properties = ["admin", "chat", "console", "desktop"] resources = [ @@ -340,6 +389,7 @@ Unlike other chat apps, you are not dependent on one specific service \ provider, and your privacy is gauranteed more than ever before. """ android = "https://kaidan.im/download/#android-experimental" +bsd = "https://www.freshports.org/net-im/kaidan/" linux = "https://kaidan.im/download/#linux" properties = ["chat", "desktop", "graphical", "mobile", "omemo"] resources = [ @@ -381,6 +431,7 @@ all of their instant messaging systems. The interface puts people first, and \ is integrated with the system address book to let you access your contacts \ from other KDE applications. """ +bsd = "https://www.freshports.org/net-im/kopete/" linux = "https://apps.kde.org/kopete/" properties = ["chat", "desktop", "graphical", "otr"] resources = [ @@ -391,21 +442,51 @@ resources = [ { url = "https://userbase.kde.org/Kopete", txt = "Project homepage" }, ] -#[mcabber] -#title = "MCabber" -#about = """ -#mcabber is a small XMPP (Jabber) console client. -# -#mcabber includes features such as SASL/SSL/TLS support, MUC (Multi-User Chat) \ -#support, history logging, command completion, OpenPGP encryption, OTR (Off-the-\ -#Record Messaging) support, dynamic modules and external action triggers. -#""" -#posix = "https://mcabber.com" -#properties = ["admin", "chat", "console", "desktop", "openpgp", "otr"] -#resources = [ -# { url = "https://mcabber.com/hg/", txt = "Project repository" }, -# { url = "https://mcabber.com", txt = "Project homepage" }, -#] +[leechcraft] +title = "LeechCraft" +about = """ +LeechCraft is a free open source cross-platform modular live environment. + +It has modules for everything, which include an HTML browser; a multiprotocol \ +modular IM client with support for encryption and audio calls; a \ +collection-oriented media player with social features like recommended artists \ +and nearby events; a BitTorrent client; a document viewer (ePUB, DjVu, PDF, \ +MOBI, etc.); an RSS feed reader with extensive support for Broadcatching and \ +podcasts ; a package manager with its own repository of plugins, themes, icons \ +and much more. + +The “Summary” tab that displays all your downloads, updates and statuses (like \ +new articles in news feeds). + +LeechCraft is a modular system, and by installing different modules you can \ +customize the feature set, keeping off the things you do not need and have a \ +decent IM client, media player or a feed reader, for example. +""" +apple = "https://leechcraft.org/download#mac-os-x" +linux = "https://leechcraft.org/download#linux" +windows = "https://leechcraft.org/download#microsoft-windows" +properties = ["chat", "desktop", "graphical", "openpgp", "otr", "plugin", "pubsub"] +resources = [ + { url = "https://github.com/0xd34df00d/leechcraft", txt = "Project repository" }, + { url = "https://leechcraft.org", txt = "Project homepage" }, +] + +[mcabber] +title = "MCabber" +about = """ +mcabber is a small XMPP (Jabber) console client. + +mcabber includes features such as SASL/SSL/TLS support, MUC (Multi-User Chat) \ +support, history logging, command completion, OpenPGP encryption, OTR (Off-the-\ +Record Messaging) support, dynamic modules and external action triggers. +""" +bsd = "https://www.freshports.org/net-im/mcabber/" +posix = "https://mcabber.com" +properties = ["admin", "chat", "console", "desktop", "featured", "openpgp", "otr"] +resources = [ + { url = "https://mcabber.com/hg/", txt = "Project repository" }, + { url = "https://mcabber.com", txt = "Project homepage" }, +] [miranda] title = "Miranda NG" @@ -418,7 +499,7 @@ client Miranda IM. It is very light on system resources and extremely fast. """ windows = "https://miranda-ng.org/downloads/" -properties = ["adhoc", "chat", "desktop", "featured", "graphical", "omemo", "openpgp", "otr"] +properties = ["adhoc", "chat", "desktop", "featured", "graphical", "omemo", "openpgp", "otr", "plugin"] resources = [ { url = "https://github.com/miranda-ng/miranda-ng", txt = "Project repository" }, { url = "https://miranda-ng.org", txt = "Project homepage" }, @@ -515,8 +596,10 @@ client optimized for business and organizations implemented as a cross-\ platform browser extension. """ browser = "https://igniterealtime.org/projects/pade/" -properties = ["chat", "desktop", "extension", "graphical", "mobile", "omemo"] +properties = ["adhoc", "chat", "desktop", "extension", "graphical", "mobile", "omemo", "pwa"] resources = [ + { url = "https://discourse.igniterealtime.org/c/pade", txt = "Support forum" }, + { url = "xmpp:open_chat@conference.igniterealtime.org?join", txt = "Support group chat" }, { url = "https://igniterealtime.org/projects/pade/", txt = "Project homepage" }, ] @@ -535,9 +618,10 @@ Pidgin supports many features of these chat networks, such as file transfers, \ away messages, buddy icons, custom smileys, and typing notifications. Numerous \ plugins also extend functionality above and beyond the standard features. """ +bsd = "https://www.freshports.org/net-im/pidgin/" linux = "https://pidgin.im/install/" windows = "https://pidgin.im/install/" -properties = ["chat", "desktop", "openpgp", "otr"] +properties = ["chat", "desktop", "openpgp", "otr", "plugin"] resources = [ { url = "https://flathub.org/apps/im.pidgin.Pidgin", txt = "Flathub package" }, { url = "https://pidgin.im", txt = "Project homepage" }, @@ -560,7 +644,7 @@ anonymous spirit of IRC while using a powerful, standard and open protocol. """ haiku = "https://depot.haiku-os.org/poezio" posix = "https://poez.io/en/#download" -properties = ["adhoc", "chat", "console", "desktop", "featured", "haikudepot", "omemo", "openpgp", "otr"] +properties = ["adhoc", "chat", "console", "desktop", "featured", "haikudepot", "omemo", "openpgp", "otr", "plugin"] resources = [ { url = "https://flathub.org/apps/io.poez.Poezio", txt = "Flathub package" }, { url = "https://codeberg.org/poezio/poezio", txt = "Project repository" }, @@ -581,8 +665,9 @@ It supports XMPP chat services, MUC chat room, OTR, PGP and OMEMO encryption, \ roster management, including flexible resource and priority settings, desktop \ notifications, and it has a plugins system in Python and C. """ +bsd = "https://www.freshports.org/net-im/profanity/" posix = "https://profanity-im.github.io" -properties = ["adhoc", "chat", "console", "desktop", "featured", "omemo", "openpgp", "otr"] +properties = ["adhoc", "chat", "console", "desktop", "featured", "omemo", "openpgp", "otr", "plugin"] resources = [ { url = "https://github.com/profanity-im/profanity", txt = "Project repository" }, { url = "https://profanity-im.github.io/plugins.html", txt = "Plugins list" }, @@ -617,7 +702,7 @@ supported operating system. apple = "https://psi-im.org" linux = "https://psi-im.org" windows = "https://psi-im.org" -properties = ["adhoc", "admin", "chat", "desktop", "graphical", "omemo", "openpgp", "otr"] +properties = ["adhoc", "admin", "chat", "desktop", "graphical", "omemo", "openpgp", "otr", "plugin"] resources = [ { url = "https://github.com/psi-im/psi", txt = "Project repository" }, { url = "xmpp:psi-dev@conference.jabber.ru?join", txt = "Support group chat" }, @@ -637,7 +722,7 @@ apple = "https://psi-plus.com/wiki/en:downloads#macos" haiku = "https://depot.haiku-os.org/psi_plus" linux = "https://psi-plus.com/wiki/en:downloads#linux" windows = "https://psi-plus.com/wiki/en:downloads#ms_windows" -properties = ["adhoc", "admin", "chat", "desktop", "featured", "graphical", "haikudepot", "omemo", "openpgp", "otr"] +properties = ["adhoc", "admin", "chat", "desktop", "featured", "graphical", "haikudepot", "omemo", "openpgp", "otr", "plugin"] resources = [ { url = "https://github.com/psi-plus/main", txt = "Project repository" }, { url = "xmpp:psi-dev@conference.jabber.ru?join", txt = "Support group chat" }, @@ -673,38 +758,40 @@ resources = [ { url = "https://pulkomandy.tk/projects/renga", txt = "Project homepage" }, ] -#[siskin] -#title = "Siskin" -#about = """ -#Siskin IM by Tigase, Inc. is a lightweight and powerful XMPP client for iPhone \ -#and iPad. It provides an easy way to talk and share moments with your friends. -#""" -#apple = "https://siskin.im/#about" -#properties = ["chat", "graphical", "mobile", "omemo"] -#resources = [ -# { url = "https://itunes.apple.com/us/app/tigase-messenger/id1153516838", txt = "iOS package" }, -# { url = "https://siskin.im", txt = "Project homepage" }, -#] +[siskin] +title = "Siskin" +about = """ +Siskin IM by Tigase, Inc. is a lightweight and powerful XMPP client for iPhone \ +and iPad. It provides an easy way to talk and share moments with your friends. +""" +apple = "https://siskin.im/#about" +properties = ["chat", "graphical", "mobile", "omemo"] +resources = [ + { url = "https://itunes.apple.com/us/app/tigase-messenger/id1153516838", txt = "iOS package" }, + { url = "https://siskin.im", txt = "Project homepage" }, +] -#[spark] -#title = "Spark" -#about = """ -#Spark is an Open Source, cross-platform IM client optimized for businesses and \ -#organizations. -# -#It features built-in support for group chat, telephony integration, and strong \ -#security. -# -#It also offers a great end-user experience with features like in-line spell \ -#checking, group chat room bookmarks, and tabbed conversations. -#""" -#apple = "https://igniterealtime.org/projects/spark/" -#linux = "https://igniterealtime.org/projects/spark/" -#windows = "https://igniterealtime.org/projects/spark/" -#properties = ["chat", "desktop", "graphical", "omemo"] -#resources = [ -# { url = "https://igniterealtime.org/projects/spark/", txt = "Project homepage" }, -#] +[spark] +title = "Spark" +about = """ +Spark is an Open Source, cross-platform IM client optimized for businesses and \ +organizations. + +It features built-in support for group chat, telephony integration, and strong \ +security. + +It also offers a great end-user experience with features like in-line spell \ +checking, group chat room bookmarks, and tabbed conversations. +""" +apple = "https://igniterealtime.org/projects/spark/" +linux = "https://igniterealtime.org/projects/spark/" +windows = "https://igniterealtime.org/projects/spark/" +properties = ["chat", "desktop", "graphical", "omemo"] +resources = [ + { url = "https://discourse.igniterealtime.org/c/spark", txt = "Support forum" }, + { url = "xmpp:open_chat@conference.igniterealtime.org?join", txt = "Support group chat" }, + { url = "https://igniterealtime.org/projects/spark/", txt = "Project homepage" }, +] #[speeqe] #title = "Speeqe" @@ -777,7 +864,6 @@ linux = "https://swift.im/downloads.html" windows = "https://swift.im/downloads.html" properties = ["chat", "desktop", "graphical"] resources = [ - { url = "xmpp:swift@rooms.swift.im?join", txt = "Support group chat" }, { url = "https://swift.im", txt = "Project homepage" }, ] @@ -813,7 +899,7 @@ currently has a minimal but ideally maximal set of XEPs. """ haiku = "https://depot.haiku-os.org/weechat" posix = "https://github.com/bqv/weechat-xmpp" -properties = ["chat", "console", "desktop", "omemo", "openpgp"] +properties = ["chat", "console", "desktop", "omemo", "openpgp", "plugin"] resources = [ { url = "https://github.com/bqv/weechat-xmpp", txt = "weechat-xmpp project repository" }, { url = "https://weechat.org", txt = "WeeChat homepage" }, @@ -831,24 +917,31 @@ interoperable open standards. """ browser = "https://xabber.com" android = "https://xabber.com" -properties = ["chat", "desktop", "featured", "graphical", "omemo", "mobile"] +properties = ["chat", "desktop", "featured", "graphical", "mobile"] resources = [ + { url = "https://www.xabber.com/rel/client/android/last/xabber.apk", txt = "Nightly package" }, { url = "https://f-droid.org/packages/com.xabber.android", txt = "F-Droid package" }, - { url = "https://play.google.com/store/apps/details?id=com.xabber.android", txt = "Play package" }, { url = "https://web.xabber.com", txt = "Xabber Web" }, { url = "https://xabber.com", txt = "Project homepage" }, ] -#[xmpp-web] -#title = "XMPP Web" -#about = """ -#Lightweight HTML chat client for XMPP servers. -#""" -#browser = "https://github.com/nioc/xmpp-web/releases" -#properties = ["chat", "desktop", "graphical", "mobile", "pwa"] -#resources = [ -# { url = "https://github.com/nioc/xmpp-web", txt = "Project homepage" }, -#] +[xmpp-web] +title = "XMPP Web" +about = """ +A lightweight HTML chat client for XMPP servers. + +It supports WebSocket, groupchat, roster, bookmarks, file transfer, password \ +protected rooms, chat state notifications, formatting of messages, stickers, \ +room creation and configuration, message moderation, vCard information. + +It is lightweight (600 KB gzipped at the first loading and then less than 10 KB) +Guest access /guest?join={jid} (joining a MUC anonymously as described in RFC 4505) +""" +browser = "https://github.com/nioc/xmpp-web/releases" +properties = ["chat", "desktop", "graphical", "mobile", "pwa"] +resources = [ + { url = "https://github.com/nioc/xmpp-web", txt = "Project repository" }, +] [yaxim] title = "yaxim" diff --git a/css/stylesheet.css b/css/stylesheet.css index 10a7d6b..c65bf2a 100644 --- a/css/stylesheet.css +++ b/css/stylesheet.css @@ -2,7 +2,9 @@ #action, #action-bar, #graphics, -#input { +#input, +.vcard-link, +.system-menu { user-select: none; } @@ -138,7 +140,7 @@ label, #number-of-pages > a, #number-of-pages .inactive { display: inline-block; - font-size: 1.2em; + /* font-size: 1.2em; */ min-width: 90px; padding: 0.5em; width: 15%; @@ -151,13 +153,20 @@ label, } #download, -#input { +#input, +.vcard-link, +.system-menu { border-radius: 2em; font-size: 1.34em; padding: 0.5em; } -#profile-top { +#system-title > * { + width: 18%; +} + +#profile-top, +#system-title { align-items: center; display: flex; justify-content: space-between; @@ -207,6 +216,23 @@ label, text-transform: uppercase; } +.system-menu { + font-weight: bold; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; +} + +.system-menu:first-child { + padding-left: 2em; + text-align: left; +} + +.system-menu:last-child { + padding-right: 2em; + text-align: right; +} + #action > a, #action-bar > a { padding-left: 1em; @@ -245,9 +271,14 @@ h1 { text-overflow: ellipsis; } -/* #count { margin: 1em; + /* min-height: 1.5em; */ +} + +/* +#count > a:before { + content: ' • '; } */ @@ -282,6 +313,22 @@ h1 { */ } +#vcard-links-extra { + display: grid; +} + +#vcard-links { + padding-bottom: 1em; +} + +.vcard-link { + filter: drop-shadow(0 0 0 black); + margin: 1em; + outline: solid; + outline-color: #cfcfcf; + text-decoration: none; +} + #profile #photo { /* object-fit: scale-down; */ } @@ -421,6 +468,30 @@ h1 { display: block; /* Show details upon checked checkbox */ } +#vcard-note-full { + color: #505050; + margin-left: 5em; + margin-right: 5em; + padding: 0 2em 2em 2em; + text-align: center; +} + +#vcard-note { + color: #505050; + margin-left: 5em; + margin-right: 5em; + /* overflow: hidden; */ + padding: 2em; + text-align: center; + /* text-overflow: ellipsis; + white-space: nowrap; */ +} + +#vcard-note:hover { + overflow: unset; + white-space: unset; +} + .plain-note { display: block; justify-content: center; @@ -469,7 +540,7 @@ h1 { margin: 1em; padding: 1.5em; text-decoration: none; - width: 20%; + width: 5em; } /* #software > .system:hover { @@ -608,11 +679,6 @@ h1 { text-decoration: none; } -.permalink { - padding-right: 0.8em; - text-decoration: none; -} - #count > a, #preview { color: #5c5656; diff --git a/fasi.py b/fasi.py index da26245..e5056d7 100644 --- a/fasi.py +++ b/fasi.py @@ -3,6 +3,7 @@ from asyncio import TimeoutError from datetime import datetime +from dateutil import parser from email.utils import parseaddr from fastapi import FastAPI, Form, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware @@ -11,7 +12,6 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates import glob #import logging -#from os import mkdir #from os.path import getsize, exists import os import qrcode @@ -215,9 +215,118 @@ class HttpInstance: response.headers['Content-Type'] = 'application/xhtml+xml' return response - # NOTE Was /b/ + @self.app.get('/c/{jid}') + async def c_jid_get(request: Request, jid): + """Display entries of a vCard4""" + jid_path = urlsplit(jid).path + if parseaddr(jid_path)[1] == jid_path: + jid_bare = jid_path.lower() + else: + jid_bare = jid + note = 'Jabber ID appears to be malformed' + + if jid_bare == jabber_id: + raise HTTPException(status_code=403, detail='access-denied') + + node_name_vcard4 = 'urn:xmpp:vcard4' + item_id_vcard4 = 'current' + + #try: + if True: + entries = [] + exception = jid_vcard = note = node_items = node_note = \ + number_of_pages = page_number = previous = selection = \ + title = None + + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name_vcard4) + filename = directory + item_id_vcard4 + '.xml' + if os.path.exists(filename) and os.path.getsize(filename) > 0: + jid_details = Data.open_file_xml(filename) + else: + await FileUtilities.cache_vcard_data( + jabber_id, password, jid_bare, node_name_vcard4, item_id_vcard4) + + xml_data = Data.open_file_xml(filename) + root_element = xml_data.getroot() + child_element = root_element[0] + #vcard_info = Syndication.extract_vcard_items(child_element) + vcard_info = Syndication.extract_vcard4_items(child_element) + + # Action and instance type + action = 'Profile' + + # Query URI links + print('Query URI links') + jid_kind = 'account' + xmpp_uri = XmppUtilities.get_xmpp_uri(jid_bare, jid_kind, node_name_vcard4) + + # Graphic files + #filename, filepath, filetype, selection = FileUtilities.handle_photo(jid_bare, jid_vcard, link_href) + + #except Exception as e: + else: + exception = str(e) + action = 'Error' + title = 'Slixmpp error' + xmpp_uri = note = jid + filename = jid_bare = link_href = link_tex = node_note = \ + node_title = number_of_pages = page_number = previous = \ + selection = url = None + + if 'fn' in vcard_info and vcard_info['fn']: + title = vcard_info['fn'] + elif 'alias' in vcard_info and vcard_info['alias']: + title = vcard_info['alias'] + else: + title = jid_bare.split('@')[0] + + if 'alias' in vcard_info and vcard_info['alias']: + alias = vcard_info['alias'] + else: + alias = jid_bare.split('@')[0] + + #if title == 'remote-server-timeout': + # raise HTTPException(status_code=408, detail='remote-server-timeout') + #else: + template_file = 'vcard.xhtml' + template_dict = { + 'action' : action, + 'alias' : alias, + 'brand_name' : brand_name, + 'brand_site' : brand_site, + 'chat_client' : chat_client, + 'entries' : entries, + 'exception' : exception, + #'filename' : filename, + 'jid_bare' : jid, + 'jid_note' : note, + #'jid_title' : title, + #'node_title' : node_title, + 'node_name' : node_name_vcard4, + 'number_of_pages' : number_of_pages, + 'page_number' : page_number, + 'previous' : previous, + 'request' : request, + 'title' : title, + 'url' : request.url._url, + 'vcard_info' : vcard_info, + 'xmpp_uri' : xmpp_uri} + response = templates.TemplateResponse(template_file, template_dict) + response.headers['Content-Type'] = 'application/xhtml+xml' + return response + + @self.app.get('/b/{jid}') + async def b_jid_get(request: Request, jid): + response = await browse_jid_node_get(request, jid, 'urn:xmpp:microblog:0') + return response + + # TODO Change to /p/ for pubsub @self.app.get('/d/{jid}/{node_name}') @self.app.get('/d/{jid}/{node_name}/{item_id}') + async def d_jid_node_get(request: Request, jid, node_name, item_id=None): + response = await browse_jid_node_get(request, jid, node_name, item_id=None) + return response + async def browse_jid_node_get(request: Request, jid, node_name, item_id=None): """Browse items of a pubsub node""" jid_path = urlsplit(jid).path @@ -232,9 +341,8 @@ class HttpInstance: #try: if True: - entries = [] - exception = jid_vcard = note = node_note = number_of_pages = \ - page_number = previous = selection = None + exception = jid_vcard = note = node_items = node_note = \ + number_of_pages = page_number = previous = selection = None filename = 'details/{}.toml'.format(jid_bare) if os.path.exists(filename) and os.path.getsize(filename) > 0: @@ -243,30 +351,20 @@ class HttpInstance: jid_details = await FileUtilities.cache_jid_data( jabber_id, password, jid_bare, node_name, item_id) - xmpp_instance = XmppInstance(jabber_id, password, jid_bare) - xmpp_instance.connect() - # Node item IDs nodes = jid_details['nodes'] - items = jid_details['items'] - if node_name not in nodes: - nodes[node_name] = {} - node_item_ids = await XmppXep0060.get_node_item_ids( - xmpp_instance, jid_bare, node_name) - #node_item_ids = await XmppUtilities.get_item_ids_of_node( - # jabber_id, password, jid_bare, node_name, nodes) - if isinstance(node_item_ids['iq'], stanza.iq.Iq): - iq = node_item_ids['iq'] - iq_disco_items_items = iq['disco_items']['items'] - for item in items: - if item[1] == node_name: - nodes[node_name]['title'] = item[2] - break - nodes[node_name]['count'] = len(iq_disco_items_items) - nodes[node_name]['item_ids'] = [] - for item in iq_disco_items_items: - nodes[node_name]['item_ids'].append( - [item[0] or '', item[1] or '', item[2] or '']) + #items = jid_details['items'] + # for item in items: + # if item[1] == node_name: + # nodes[node_name]['title'] = item[2] + # break + supdirectory = 'xep_0060/{}/'.format(jid_bare) + if not os.path.exists(supdirectory): os.mkdir(supdirectory) + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name) + if not os.path.exists(directory): + os.mkdir(directory) + await FileUtilities.cache_node_data( + jabber_id, password, jid_bare, node_name) count = jid_details['count'] jid_info = { @@ -279,7 +377,8 @@ class HttpInstance: 'note' : jid_details['note'], 'type' : jid_details['image_type']} messages = jid_details['messages'] - node_title = nodes[node_name]['title'] + #node_title = nodes[node_name]['title'] if 'title' in nodes[node_name] else jid_details['name'] + node_title = node_name note = jid_details['note'] #title = nodes[node_name]['title'] if node_name else jid_details['name'] title = jid_details['name'] @@ -290,50 +389,36 @@ class HttpInstance: #xmpp_uri = '{}?;node={}'.format(jid_bare, node_name) # Node items - if item_id: - previous = True - node_items = await XmppXep0060.get_node_items( - xmpp_instance, jid_bare, node_name, item_ids=[item_id]) - else: - item_ids = [] - for item in nodes[node_name]['item_ids']: - item_ids.append(item[2]) - # NOTE Consider to neglect the reversal of order, because, then, items can be found at the same page. - item_ids.reverse() - page_number = request.query_params.get('page', '') - if page_number: - try: - page_number = int(page_number) - ix = (page_number -1) * 10 - except: - ix = 0 - page_number = 1 - else: + entries = [] + node_items = os.listdir(directory) + if 'urn:xmpp:avatar:metadata.xml' in node_items: + node_items.remove('urn:xmpp:avatar:metadata.xml') + page_number = request.query_params.get('page', '') + if page_number: + try: + page_number = int(page_number) + ix = (page_number -1) * 10 + except: ix = 0 page_number = 1 - item_ids_10 = item_ids[ix:][:10] - number_of_pages = int(len(item_ids) / 10) - if number_of_pages < len(item_ids) / 10: number_of_pages += 1 - node_items = await XmppXep0060.get_node_items( - xmpp_instance, jid_bare, node_name, item_ids=item_ids_10) - - if isinstance(node_items['iq'], stanza.iq.Iq): - #title = title or node_name - if not node_title: node_title = node_name - #node_note = nodes[node_name]['title'] if node_name else jid_details['name'] - #node_note = xmpp_uri # jid_bare - iq = node_items['iq'] - for item in iq['pubsub']['items']: - item_payload = item['payload'] - entry = Syndication.extract_items(item_payload) - if entry: entry['id'] = item['id'] - entries.append(entry) - #if len(entries) > 10: break else: - message = '{}: {} (XEP-0060)'.format(node_items['condition'], node_items['text']) - if entries: entries.reverse() - - xmpp_instance.disconnect() + ix = 0 + page_number = 1 + item_ids_10 = node_items[ix:][:10] + number_of_pages = int(len(node_items) / 10) + if number_of_pages < len(node_items) / 10: number_of_pages += 1 + if node_items: + for item in item_ids_10: + filename = directory + item + xml_data = Data.open_file_xml(filename) + root_element = xml_data.getroot() + child_element = root_element[0] + entry = Syndication.extract_atom_items(child_element) + if entry: + filename_without_file_extension = item[:len(item)-4] + entry['id'] = filename_without_file_extension + entries.append(entry) + #if len(entries) > 10: break if jid_kind: # Action and instance type @@ -540,9 +625,9 @@ class HttpInstance: #try: if True: - action = count = exception = instance = jid_vcard = \ - jid_info = link_href = message = note = selection = title = \ - view_href = xmpp_uri = None + action = alias = count_item = count_message = exception = \ + instance = jid_vcard = jid_info = link_href = message = note = \ + selection = title = vcard4 = view_href = xmpp_uri = None #node_name = 'urn:xmpp:microblog:0' filename = 'details/{}.toml'.format(jid_bare) @@ -555,19 +640,13 @@ class HttpInstance: # Set node name to 'urn:xmpp:microblog:0' jid_kind = jid_details['kind'] nodes = jid_details['nodes'] + count_message = jid_details['messages'] if (jid_kind not in ('conference', 'mix', 'muc') and '@' in jid_bare and not node_name and 'urn:xmpp:microblog:0' in nodes): node_name = 'urn:xmpp:microblog:0' - if ('@' in jid_bare and - 'urn:xmpp:microblog:0' not in nodes and - jid_kind not in ('conference', 'mix', 'muc')): - count = 0 - else: - count = nodes[node_name]['count'] if node_name in nodes else jid_details['count'] - items = jid_details['items'] jid_info = { 'error' : jid_details['error'], @@ -581,20 +660,49 @@ class HttpInstance: #note = nodes[node_name]['title'] if node_name in nodes else jid_details['note'] note = jid_details['note'] - # TODO Append results to file + # vCard4 + node_name_vcard4 = 'urn:xmpp:vcard4' + item_id_vcard4 = 'current' + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name_vcard4) + filename = directory + item_id_vcard4 + '.xml' + if os.path.exists(filename) and os.path.getsize(filename) > 0: + xml_data = Data.open_file_xml(filename) + root_element = xml_data.getroot() + child_element = root_element[0] + #vcard_info = Syndication.extract_vcard_items(child_element) + vcard_info = Syndication.extract_vcard4_items(child_element) + title = vcard_info['fn'] + alias = vcard_info['alias'] + note = vcard_info['note'] + else: + await FileUtilities.cache_vcard_data( + jabber_id, password, jid_bare, node_name_vcard4, item_id_vcard4) + + if os.path.exists(filename) and os.path.getsize(filename) > 0: + vcard4 = True + # Node item IDs - if node_name not in nodes: - nodes[node_name] = await XmppUtilities.get_item_ids_of_node( - jabber_id, password, jid_bare, node_name, nodes) - if isinstance(nodes[node_name]['iq'], stanza.iq.Iq): - iq = nodes[node_name]['iq'] - iq_disco_items = iq['disco_items'] - if iq_disco_items['items']: - count = len(nodes[node_name]['iq']['disco_items']['items']) - else: - count = 0 - else: - count = 0 + supdirectory = 'xep_0060/{}/'.format(jid_bare) + if not os.path.exists(supdirectory): os.mkdir(supdirectory) + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name) + if not os.path.exists(directory): + os.mkdir(directory) + await FileUtilities.cache_node_data( + jabber_id, password, jid_bare, node_name) + + # Node items + entries = [] + node_items = os.listdir(directory) + if 'urn:xmpp:avatar:metadata.xml' in node_items: + node_items.remove('urn:xmpp:avatar:metadata.xml') + count_item = len(node_items) + +# if ('@' in jid_bare and +# 'urn:xmpp:microblog:0' not in nodes and +# jid_kind not in ('conference', 'mix', 'muc')): +# count_item = 0 +# else: +# count_item = len(node_items) if jid_kind == 'pubsub' and node_name: items = jid_details['items'] @@ -616,7 +724,7 @@ class HttpInstance: else: # jid_info['error'] action = 'Contact' instance = view_href = '' - message = '{}: {} (XEP-0030)'.format(jid_info['text'], jid_info['condition']) + if jid_info['condition']: message = '{}: {} (XEP-0030)'.format(jid_info['text'], jid_info['condition']) xmpp_uri = jid_bare link_href = XmppUtilities.get_link_href(jid_bare, jid_kind, node_name) @@ -635,8 +743,12 @@ class HttpInstance: action = 'Error' title = 'Slixmpp error' xmpp_uri = jid - count = filename = jid_bare = jid_vcard = jid_kind = links = \ - message = selection = url = None + alias = count_item = count_message = filename = jid_bare = \ + jid_vcard = jid_kind = links = message = selection = url = \ + vcard4 = None + + note_500 = note[:500] + note = note_500 + ' …' if note_500 < note else note_500 # NOTE Handling of variables "title" and "note" in case of '/j/{jid}/{node_name}' is confusing. # TODO Add new keys that are of 'node' and be utilized for nodes, instead of reusing a variable for several roles. @@ -645,10 +757,12 @@ class HttpInstance: template_file = 'jid.xhtml' template_dict = { 'action' : action, + 'alias' : alias, 'brand_name' : brand_name, 'brand_site' : brand_site, 'chat_client' : chat_client, - 'count' : count, + 'count_item' : count_item, + 'count_message' : count_message, 'instance' : instance, 'exception' : exception, 'filename' : filename, @@ -656,11 +770,13 @@ class HttpInstance: 'jid_kind' : jid_kind, 'links' : links, 'message' : message, + 'news_client' : news_client, 'note' : note, # TODO node_note or title of PubSub JID 'request' : request, 'selection' : selection, 'title' : title, # TODO node_title 'url' : request.url._url, + 'vcard4' : vcard4, 'view_href' : view_href, 'xmpp_uri' : xmpp_uri} response = templates.TemplateResponse(template_file, template_dict) @@ -710,6 +826,8 @@ class HttpInstance: user_agent = request.headers.get("user-agent") user_agent_lower = user_agent.lower() match user_agent_lower: + case _ if 'bsd' in user_agent_lower: + software = 'bsd' case _ if 'linux' in user_agent_lower: software = 'linux' case _ if 'haiku' in user_agent_lower: @@ -722,14 +840,18 @@ class HttpInstance: software = 'apple' name = software.title() + if software == 'bsd': name = 'BSD' if software == 'posix': name = 'POSIX' if software == 'ubports': name = 'UBports' + if name.endswith('os'): name = name.replace('os', 'OS') filename_clients = 'clients.toml' clients = Data.open_file_toml(filename_clients) client_selection = [] + clients_software = 0 for client in clients: if software in clients[client]: + clients_software += 1 if featured and 'featured' not in clients[client]['properties']: skipped = True continue @@ -742,12 +864,15 @@ class HttpInstance: 'resources' : clients[client]['resources'] if 'resources' in clients[client] else ''} client_selection.append(client_selected) + skipped = False if len(client_selection) == clients_software else True + template_file = 'download.xhtml' template_dict = { 'brand_name' : brand_name, 'brand_site' : brand_site, 'chat_client' : chat_client, 'client_selection' : client_selection, + 'featured' : featured, 'skipped' : skipped, 'request' : request, 'software' : software, @@ -824,9 +949,81 @@ class Data: data_as_string = tomli_w.dumps(data) fn.write(data_as_string) + def open_file_xml(filename: str) -> ET.ElementTree: + data = ET.parse(filename) + return data + + def save_to_file(filename: str, data: str) -> None: + with open(filename, 'w') as fn: + fn.write(data) + class FileUtilities: - async def cache_jid_data(jabber_id, password, jid_bare, node_name=None, item_id=None, alias=None): + async def cache_vcard_data( + jabber_id, password, jid_bare, node_name_vcard4, item_id_vcard4): + + # Start an XMPP instance and retrieve information + xmpp_instance = XmppInstance(jabber_id, password, jid_bare) + xmpp_instance.connect() + + vcard4_data = await XmppXep0060.get_node_items( + xmpp_instance, jid_bare, node_name_vcard4, item_ids=[item_id_vcard4]) + + xmpp_instance.disconnect() + + if vcard4_data: + supdirectory = 'xep_0060/{}/'.format(jid_bare) + if not os.path.exists(supdirectory): os.mkdir(supdirectory) + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name_vcard4) + if not os.path.exists(directory): os.mkdir(directory) + if isinstance(vcard4_data['iq'], stanza.iq.Iq): + iq = vcard4_data['iq'] + for item in iq['pubsub']['items']: + filename = directory + item_id_vcard4 + '.xml' + xml_item_as_string = str(item) + Data.save_to_file(filename, xml_item_as_string) + #item_payload = item['payload'] + #vcard4_info = Syndication.extract_vcard4_items(item_payload) + + async def cache_node_data( + jabber_id, password, jid_bare, node_name): + + # Start an XMPP instance and retrieve information + xmpp_instance = XmppInstance(jabber_id, password, jid_bare) + xmpp_instance.connect() + + node_items = await XmppXep0060.get_node_items( + xmpp_instance, jid_bare, node_name) + + xmpp_instance.disconnect() + + if node_items: + supdirectory = 'xep_0060/{}/'.format(jid_bare) + if not os.path.exists(supdirectory): os.mkdir(supdirectory) + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name) + if not os.path.exists(directory): os.mkdir(directory) + if isinstance(node_items['iq'], stanza.iq.Iq): + iq = node_items['iq'] + namespace = '{http://www.w3.org/2005/Atom}' + for item in iq['pubsub']['items']: + item_payload = item['payload'] + date_element = item_payload.find(namespace + 'updated') + if not date_element: date_element = item_payload.find(namespace + 'published') + if isinstance(date_element, ET.Element): + date = date_element.text + modification_time = parser.parse(date).timestamp() + filename = directory + item['id'] + '.xml' + xml_item_as_string = str(item) + Data.save_to_file(filename, xml_item_as_string) + if isinstance(date_element, ET.Element): + file_statistics = os.stat(filename) + access_time = file_statistics.st_atime + os.utime(filename, (access_time, modification_time)) + #item_payload = item['payload'] + #entry = Syndication.extract_atom_items(item_payload) + + async def cache_jid_data( + jabber_id, password, jid_bare, node_name=None, item_id=None, alias=None): iq_disco_items_list = iq_disco_items_items_list = node_note = node_title = title = '' jid_vcard = { @@ -849,6 +1046,28 @@ class FileUtilities: jid_info_iq = jid_info['iq'] jid_kind = jid_info['kind'] + # Set node name to 'urn:xmpp:microblog:0' if JID is an account + if jid_kind == 'account' and not node_name: node_name = 'urn:xmpp:microblog:0' + + # vCard4 data + node_name_vcard4 = 'urn:xmpp:vcard4' + item_id_vcard4 = 'current' + vcard4_data = await XmppXep0060.get_node_items( + xmpp_instance, jid_bare, node_name_vcard4, item_ids=[item_id_vcard4]) + if vcard4_data: + supdirectory = 'xep_0060/{}/'.format(jid_bare) + if not os.path.exists(supdirectory): os.mkdir(supdirectory) + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name_vcard4) + if not os.path.exists(directory): os.mkdir(directory) + if isinstance(vcard4_data['iq'], stanza.iq.Iq): + iq = vcard4_data['iq'] + for item in iq['pubsub']['items']: + filename = directory + item_id_vcard4 + '.xml' + xml_item_as_string = str(item) + Data.save_to_file(filename, xml_item_as_string) + #item_payload = item['payload'] + #vcard4_info = Syndication.extract_vcard4_items(item_payload) + # JID info print('JID info') # NOTE Group chat of Psi+ Project at jabber.ru has a note in its vCard. @@ -976,6 +1195,38 @@ class FileUtilities: nodes[node_name]['item_ids'].append( [item_id[0] or '', item_id[1] or '', item_id[2] or '']) + item_ids = [] + for item in nodes[node_name]['item_ids']: + item_ids.append(item[2]) + + node_items = await XmppXep0060.get_node_items( + xmpp_instance, jid_bare, node_name) + + if node_items: + supdirectory = 'xep_0060/{}/'.format(jid_bare) + if not os.path.exists(supdirectory): os.mkdir(supdirectory) + directory = 'xep_0060/{}/{}/'.format(jid_bare, node_name) + if not os.path.exists(directory): os.mkdir(directory) + if isinstance(node_items['iq'], stanza.iq.Iq): + iq = node_items['iq'] + namespace = '{http://www.w3.org/2005/Atom}' + for item in iq['pubsub']['items']: + item_payload = item['payload'] + date_element = item_payload.find(namespace + 'updated') + if not date_element: date_element = item_payload.find(namespace + 'published') + if isinstance(date_element, ET.Element): + date = date_element.text + modification_time = parser.parse(date).timestamp() + filename = directory + item['id'] + '.xml' + xml_item_as_string = str(item) + Data.save_to_file(filename, xml_item_as_string) + if isinstance(date_element, ET.Element): + file_statistics = os.stat(filename) + access_time = file_statistics.st_atime + os.utime(filename, (access_time, modification_time)) + #item_payload = item['payload'] + #entry = Syndication.extract_atom_items(item_payload) + xmpp_instance.disconnect() # Notes @@ -1175,36 +1426,202 @@ class Graphics: class Syndication: - def extract_items(item_payload, limit=False): +# def extract_vcard_items(xml_data): +# namespace = '{urn:ietf:params:xml:ns:vcard-4.0}' +# title = xml_data.find(namespace + 'title') +# +# entry = {'fn' : content_text, +# 'note' : link_href, +# 'email' : published_text, +# 'impp' : summary_text, +# 'url' : tags} +# return entry + + def extract_vcard_items(xml_data): + """Extracts all items from a vCard XML ElementTree. + + Args: + xml_data (ElementTree): The vCard XML as an ElementTree object. + + Returns: + dict: A dictionary where keys are item names and values are their text content. + """ + + items = {} + for item in xml_data.iter(): + # Skip the root element (vcard) + if item.tag == '{urn:ietf:params:xml:ns:vcard-4.0}vcard': + continue + + # Extract item name and text content + item_name = item.tag.split('}')[1] + + # Check for any direct text content or child elements + item_text = [] + if item.text: + item_text.append(item.text) + for child in item: + if child.text: + item_text.append(child.text) + + # Join text elements if multiple found + if item_text: + items[item_name] = ' '.join(item_text).strip() # Strip extra spaces + else: + items[item_name] = None + + return items + + def extract_vcard4_items(xml_data): + namespace = '{urn:ietf:params:xml:ns:vcard-4.0}' + vcard = {} + + element_em = xml_data.find(namespace + 'email') + element_fn = xml_data.find(namespace + 'fn') + element_nn = xml_data.find(namespace + 'nickname') + element_nt = xml_data.find(namespace + 'note') + element_og = xml_data.find(namespace + 'org') + element_im = xml_data.find(namespace + 'impp') + element_ul = xml_data.find(namespace + 'url') + + if isinstance(element_em, ET.Element): + for i in element_em: + text = i.text + if text: + email = text + break + else: + email = '' + else: + email = '' + if isinstance(element_fn, ET.Element): + for i in element_fn: + text = i.text + if text: + title = text + break + else: + title = '' + else: + title = '' + if isinstance(element_nn, ET.Element): + for i in element_nn: + text = i.text + if text: + alias = text + break + else: + alias = '' + else: + alias = '' + if isinstance(element_nt, ET.Element): + for i in element_nt: + text = i.text + if text: + note = text + break + else: + note = '' + else: + note = '' + if isinstance(element_og, ET.Element): + for i in element_og: + text = i.text + if text: + org = text + break + else: + org = '' + else: + org = '' + if isinstance(element_im, ET.Element): + for i in element_im: + text = i.text + if text: + impp = text + break + else: + impp = '' + else: + impp = '' + if isinstance(element_ul, ET.Element): + for i in element_ul: + text = i.text + if text: + url = text + break + else: + url = '' + else: + url = '' + + extra_resources = { + 'code' : [], + 'gallery' : [], + 'journal' : [], + 'movim' : [], + 'peertube' : [], + } + for res in extra_resources: + try: + count = len(xml_data.findall(namespace + 'group[@name="' + res + '"]/' + namespace + 'x-ablabel')) + except: + breakpoint() + for p in range(count): + position = str(p + 1) + print(res, position) + for i in xml_data.find(namespace + 'group[@name="' + res + '"]/' + namespace + 'x-ablabel[' + position + ']'): + txt = i.text + for i in xml_data.find(namespace + 'group[@name="' + res + '"]/' + namespace + 'url[' + position + ']'): + uri = i.text + extra_resources[res].append({'label' : txt, 'uri' : uri}) + vcard[res] = extra_resources[res] + + vcard['alias'] = alias + vcard['email'] = email + vcard['fn'] = title + vcard['note'] = note + vcard['org'] = org + vcard['impp'] = impp + vcard['url'] = url + return vcard + + + def extract_atom_items(xml_data, limit=False): + # NOTE + # `.//` was not needded when node item payload was passed directly. + # Now that item is saved as xml, it is required to use `.//`. + # Perhaps navigating a level down (i.e. to "child"), or removing the root from the file would solve this. + #namespace = './/{http://www.w3.org/2005/Atom}' namespace = '{http://www.w3.org/2005/Atom}' - title = item_payload.find(namespace + 'title') - links = item_payload.find(namespace + 'link') + title = xml_data.find(namespace + 'title') + links = xml_data.find(namespace + 'link') if (not isinstance(title, ET.Element) and not isinstance(links, ET.Element)): return None title_text = '' if title == None else title.text link_href = '' if isinstance(links, ET.Element): - for link in item_payload.findall(namespace + 'link'): + for link in xml_data.findall(namespace + 'link'): link_href = link.attrib['href'] if 'href' in link.attrib else '' if link_href: break - contents = item_payload.find(namespace + 'content') + contents = xml_data.find(namespace + 'content') content_text = '' if isinstance(contents, ET.Element): - for content in item_payload.findall(namespace + 'content'): + for content in xml_data.findall(namespace + 'content'): content_text = content.text or '' if content_text: break - summaries = item_payload.find(namespace + 'summary') + summaries = xml_data.find(namespace + 'summary') summary_text = '' if isinstance(summaries, ET.Element): - for summary in item_payload.findall(namespace + 'summary'): + for summary in xml_data.findall(namespace + 'summary'): summary_text = summary.text or '' if summary_text: break - published = item_payload.find(namespace + 'published') + published = xml_data.find(namespace + 'published') published_text = '' if published == None else published.text - categories = item_payload.find(namespace + 'category') + categories = xml_data.find(namespace + 'category') tags = [] if isinstance(categories, ET.Element): - for category in item_payload.findall(namespace + 'category'): + for category in xml_data.findall(namespace + 'category'): if 'term' in category.attrib and category.attrib['term']: category_term = category.attrib['term'] if len(category_term) < 20: @@ -1214,7 +1631,7 @@ class Syndication: if limit and len(tags) > 4: break - identifier = item_payload.find(namespace + 'id') + identifier = xml_data.find(namespace + 'id') if identifier and identifier.attrib: print(identifier.attrib) identifier_text = '' if identifier == None else identifier.text @@ -1231,13 +1648,6 @@ class Syndication: class XmppUtilities: - async def get_item_ids_of_node(jabber_id, password, jid_bare, node_name, nodes): - xmpp_instance = XmppInstance(jabber_id, password, jid_bare) - xmpp_instance.connect() - node_item_ids = await XmppXep0060.get_node_item_ids(xmpp_instance, jid_bare, node_name) - xmpp_instance.disconnect() - return node_item_ids - def set_action_instance_type(jid_kind, node_name=None): if jid_kind in ('conference', 'server'): action = 'Discover' @@ -1363,13 +1773,13 @@ class XmppXep0030: async def get_jid_items(self, jid_bare): try: - condition = text = None + condition = text = '' error = False iq = await self['xep_0030'].get_items(jid=jid_bare) except (IqError, IqTimeout) as e: #logger.warning('Chat type could not be determined for {}'.format(jid_bare)) #logger.error(e) - iq = None + iq = '' error = True condition = e.iq['error']['condition'] text = e.iq['error']['text'] or 'Error' @@ -1393,7 +1803,7 @@ class XmppXep0030: jid_kind = None try: error = False - condition = text = None + condition = text = '' iq = await self['xep_0030'].get_info(jid=jid_bare) iq_disco_info = iq['disco_info'] if iq_disco_info: @@ -1410,17 +1820,8 @@ class XmppXep0030: 'muc_unmoderated' in features or 'muc_unsecured' in features): jid_kind = 'muc' - else: + elif '@' in jid_bare: for identity in iq_disco_info['identities']: - if identity[0] == 'pubsub' and identity[1] == 'service': - #if 'http://jabber.org/protocol/pubsub' in features: - #if 'http://jabber.org/protocol/pubsub#access-authorize' in features: - #if 'http://jabber.org/protocol/rsm' in features: - jid_kind = 'pubsub' - break - if identity[0] == 'server' and identity[1] == 'im': - jid_kind = 'server' - break #if identity[0] == 'pubsub' and identity[1] == 'pep': if identity[0] == 'account': #if 'urn:xmpp:bookmarks:1#compat-pep' in features: @@ -1436,14 +1837,25 @@ class XmppXep0030: break if identity[0] == 'client' and identity[1] == 'bot': jid_kind = 'bot' + else: + for identity in iq_disco_info['identities']: + if identity[0] == 'pubsub' and identity[1] == 'service': + #if 'http://jabber.org/protocol/pubsub' in features: + #if 'http://jabber.org/protocol/pubsub#access-authorize' in features: + #if 'http://jabber.org/protocol/rsm' in features: + jid_kind = 'pubsub' + break + if identity[0] == 'server' and identity[1] == 'im': + jid_kind = 'server' + break #logger.info('Jabber ID: {}\n' # 'Chat Type: {}'.format(jid_bare, result)) else: - iq = condition = text = None + iq = condition = text = '' except (IqError, IqTimeout) as e: #logger.warning('Chat type could not be determined for {}'.format(jid_bare)) #logger.error(e) - iq = None + iq = '' error = True condition = e.iq['error']['condition'] text = e.iq['error']['text'] or 'Error' @@ -1472,7 +1884,7 @@ class XmppXep0045: if not seconds: seconds = 864000 try: error = False - condition = text = None + condition = text = '' #since = datetime.fromtimestamp(time.time()-seconds) iq = await self['xep_0045'].join_muc_wait( jid, @@ -1519,7 +1931,7 @@ class XmppXep0054: async def get_vcard_data(self, jid_bare): try: error = False - condition = text = None + condition = text = '' iq = await self['xep_0054'].get_vcard(jid_bare) except (IqError, IqTimeout) as e: error = True @@ -1530,7 +1942,7 @@ class XmppXep0054: text = 'Could not retrieve vCard' else: text = 'Unknown Error' - iq = None + iq = '' result = { 'error' : error, 'condition' : condition, @@ -1543,7 +1955,7 @@ class XmppXep0060: async def get_node_items(self, jid_bare, node_name, item_ids=None, max_items=None): try: error = False - condition = text = None + condition = text = '' if max_items: iq = await self['xep_0060'].get_items( jid_bare, node_name, timeout=5) @@ -1561,7 +1973,7 @@ class XmppXep0060: result = iq except (IqError, IqTimeout) as e: error = True - iq = None + iq = '' condition = e.iq['error']['condition'] text = e.iq['error']['text'] if not text: @@ -1579,7 +1991,7 @@ class XmppXep0060: async def get_node_item_ids(self, jid_bare, node_name): try: error = False - condition = text = None + condition = text = '' iq = await self['xep_0030'].get_items( jid_bare, node_name) # Broken. See https://codeberg.org/poezio/slixmpp/issues/3548 @@ -1587,7 +1999,7 @@ class XmppXep0060: # jid_bare, node_name, timeout=5) except (IqError, IqTimeout) as e: error = True - iq = None + iq = '' condition = e.iq['error']['condition'] text = e.iq['error']['text'] if not text: diff --git a/img/atalk.svg b/img/atalk.svg new file mode 100644 index 0000000..e97618e --- /dev/null +++ b/img/atalk.svg @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Talk + diff --git a/img/beagle.svg b/img/beagle.svg new file mode 100644 index 0000000..068df5c --- /dev/null +++ b/img/beagle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/bruno.svg b/img/bruno.svg new file mode 100644 index 0000000..26b8b9e --- /dev/null +++ b/img/bruno.svg @@ -0,0 +1 @@ +🐻️ diff --git a/img/bsd.svg b/img/bsd.svg new file mode 100644 index 0000000..01b4efe --- /dev/null +++ b/img/bsd.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/candy.svg b/img/candy.svg new file mode 100644 index 0000000..58ce5a3 --- /dev/null +++ b/img/candy.svg @@ -0,0 +1 @@ +🍭️ diff --git a/img/chat-o-matic.svg b/img/chat-o-matic.svg new file mode 100644 index 0000000..c5a61f7 --- /dev/null +++ b/img/chat-o-matic.svg @@ -0,0 +1 @@ +💬️ diff --git a/img/convo.svg b/img/convo.svg new file mode 100644 index 0000000..779b329 --- /dev/null +++ b/img/convo.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/img/coyim.svg b/img/coyim.svg new file mode 100644 index 0000000..34af640 --- /dev/null +++ b/img/coyim.svg @@ -0,0 +1 @@ +🐠️ diff --git a/img/dergchat.svg b/img/dergchat.svg new file mode 100644 index 0000000..f9d78d6 --- /dev/null +++ b/img/dergchat.svg @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/leechcraft.svg b/img/leechcraft.svg new file mode 100644 index 0000000..cd103bd --- /dev/null +++ b/img/leechcraft.svg @@ -0,0 +1,724 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/mcabber.svg b/img/mcabber.svg new file mode 100644 index 0000000..323f9ec --- /dev/null +++ b/img/mcabber.svg @@ -0,0 +1 @@ +💻️ diff --git a/img/profanity_logo.svg b/img/profanity_logo.svg new file mode 100644 index 0000000..f6b0114 --- /dev/null +++ b/img/profanity_logo.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/siskin.svg b/img/siskin.svg new file mode 100644 index 0000000..50fd767 --- /dev/null +++ b/img/siskin.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/spark.svg b/img/spark.svg new file mode 100644 index 0000000..88418ff --- /dev/null +++ b/img/spark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/xmpp-web.svg b/img/xmpp-web.svg new file mode 100644 index 0000000..2dd5597 --- /dev/null +++ b/img/xmpp-web.svg @@ -0,0 +1 @@ +🕸️ diff --git a/systems.toml b/systems.toml index f9d5dd0..ff99bd5 100644 --- a/systems.toml +++ b/systems.toml @@ -2,6 +2,7 @@ systems = [ { name = "Android", id = "android" }, { name = "Apple", id = "apple" }, { name = "Browser", id = "browser" }, + { name = "BSD", id = "bsd" }, { name = "Haiku", id = "haiku" }, { name = "KaiOS", id = "kaios" }, { name = "Linux", id = "linux" }, diff --git a/xep_0060/README b/xep_0060/README new file mode 100644 index 0000000..3e9e39c --- /dev/null +++ b/xep_0060/README @@ -0,0 +1 @@ +This directory caches PubSub node items. diff --git a/xhtml/download.xhtml b/xhtml/download.xhtml index e221edc..825339d 100644 --- a/xhtml/download.xhtml +++ b/xhtml/download.xhtml @@ -32,7 +32,17 @@
{% if client_selection %} -

{{title}}

+
+ < Systems +

{{title}}

+ {% if skipped %} + All clients > + {% elif not featured %} + Featured > + {% else %} + + {% endif %} +
{% for client in client_selection %} @@ -158,16 +168,6 @@
{% endif %} - - {% if skipped %} -
- Display the - complete list of XMPP clients for {{title}}. -
- {% endif %}

@@ -201,6 +201,9 @@ {% if 'otr' in client['properties'] %} 🔏️ OTR {% endif %} + {% if 'plugin' in client['properties'] %} + 🧩️ Plugins + {% endif %} {% if 'pubsub' in client['properties'] %} 📡️ PubSub {% endif %} diff --git a/xhtml/jid.xhtml b/xhtml/jid.xhtml index a484745..5c76085 100644 --- a/xhtml/jid.xhtml +++ b/xhtml/jid.xhtml @@ -6,15 +6,15 @@ - {{brand_name}}: {{action}} {{title}} - + {{brand_name}}: {{action}} {% if alias %}{{alias}}{% else %}{{title}}{% endif %} + - + - + @@ -77,12 +77,12 @@
{% endif %} - {% if note %} -

{{note}}

- {% endif %} -
-
{{xmpp_uri}}
+
+ {% if note %}{{note}}{% endif %}
+ {% if exception %}
{{exception}} @@ -102,20 +102,18 @@ Preview journal OR Preview group chat
- {% if count or jid_kind in ('conference', 'mix', 'muc') %} - - {% endif %} +
+ {% if count_item or count_message %} + {% if count_item %}{{count_item}} {{instance}}{% elif count_message %}Preview{% endif %} + + {% endif %} + {% if vcard4 %} + my profile + {% endif %} +
- If you already have {% if chat_client %}{{chat_client}}{% else %}an XMPP Client{% endif %} you can + If you already have {% if news_client and jid_kind == 'pubsub' %}{{news_client}}{% elif chat_client %}{{chat_client}}{% else %}an XMPP Client{% endif %} you can
{% if jid_kind in ('conference', 'mix', 'muc') %} join to diff --git a/xhtml/node.xhtml b/xhtml/node.xhtml index 43f612a..648fb7e 100644 --- a/xhtml/node.xhtml +++ b/xhtml/node.xhtml @@ -76,14 +76,10 @@
{% endif %} {% endfor %} diff --git a/xhtml/vcard.xhtml b/xhtml/vcard.xhtml new file mode 100644 index 0000000..3689dc8 --- /dev/null +++ b/xhtml/vcard.xhtml @@ -0,0 +1,135 @@ + + + + + + + + + {{brand_name}}: {{action}} {% if alias %}{{alias}}{% else %}{{title}}{% endif %} + + + + + + + + + + + + + + + +
+ +
+
+

+ {% if 'fn' in vcard_info and vcard_info['fn'] %} + {{vcard_info['fn']}} + {% else %} + {{jid_bare}} + {% endif %} +

+ {% if 'org' in vcard_info and vcard_info['org'] %} +

{{vcard_info['org']}}

+ {% endif %} + {% if 'note' in vcard_info and vcard_info['note'] %} +
{{vcard_info['note']}}
+ {% endif %} + + +
+ {% for i in vcard_info %} + + {% endfor %} +
+ {% if exception %} +
+ {{exception}} +
+ {% endif %} + {% if links %} +
+ {% for link in links %} + + {{link['name']}} + + {% endfor %} +
+ {% endif %} + + {% if count or jid_kind in ('conference', 'mix', 'muc') %} + + {% endif %} +
+
+ {% if message %} +
{{message}}
+ {% endif %} +
+ +