Files
org-social.el/org-social-relay.el
2026-01-05 13:39:17 +01:00

813 lines
50 KiB
EmacsLisp

;;; org-social-relay.el --- Relay functionality for Org-social -*- lexical-binding: t -*-
;; SPDX-License-Identifier: GPL-3.0
;; Author: Andros Fenollosa <hi@andros.dev>
;; Version: 2.10
;; URL: https://github.com/tanrax/org-social.el
;;; Commentary:
;; Relay integration for Org-social, providing automatic feed registration
;; and endpoint discovery for enhanced social features.
;;; Code:
(require 'json)
(require 'cl-lib)
(require 'org-social-variables)
;; Declare request function
(declare-function request "request" (url &rest args))
;; Forward declarations
(declare-function url-hexify-string "url-util" (string))
(defun org-social-relay--discover-endpoints (relay-url callback)
"Discover relay endpoints by exploring the root API.
RELAY-URL is the base URL of the relay server.
Call CALLBACK with discovered endpoints."
(request relay-url
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(links (cdr (assoc '_links response)))
(endpoints (make-hash-table :test 'equal)))
;; Parse _links structure - can be either object (new format) or array (old format)
(cond
;; Check if this is the new object format by looking for 'href key in first element
((and (listp links)
(listp (car links))
(assoc 'href (cdr (car links))))
;; New format: _links is an alist with endpoint names as keys
(dolist (endpoint-pair links)
(let* ((endpoint-name (symbol-name (car endpoint-pair)))
(endpoint-data (cdr endpoint-pair))
(href (cdr (assoc 'href endpoint-data)))
(method (cdr (assoc 'method endpoint-data))))
(when (and href method)
(puthash endpoint-name (list :href href :method method) endpoints)))))
;; Old format: _links is an array of objects with rel, href, method
(t
(let ((links-list (if (vectorp links)
(append links nil)
links)))
(dolist (link links-list)
(let ((rel (cdr (assoc 'rel link)))
(href (cdr (assoc 'href link)))
(method (cdr (assoc 'method link))))
(when (and rel href method)
(puthash rel (list :href href :method method) endpoints)))))))
(funcall callback endpoints))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay API response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to discover relay endpoints: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))))))
(defun org-social-relay--register-feed ()
"Register the user's feed in the configured relay server."
(when (and org-social-relay
org-social-my-public-url
(not (string-empty-p org-social-relay))
(not (string-empty-p org-social-my-public-url)))
(let ((relay-url (string-trim-right org-social-relay "/"))
(feed-url org-social-my-public-url))
(message "Discovering relay endpoints: %s" relay-url)
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((add-feed-endpoint (gethash "add-feed" endpoints)))
(if add-feed-endpoint
(let ((href (plist-get add-feed-endpoint :href))
(method (plist-get add-feed-endpoint :method)))
(message "Registering feed using endpoint: %s %s" method href)
(request (concat relay-url href)
:type method
:data (json-encode `(("feed" . ,feed-url)))
:headers '(("Content-Type" . "application/json"))
:timeout 10
:success (cl-function
(lambda (&key _data &allow-other-keys)
(message "Feed successfully registered in relay")))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to register feed in relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))))))
(message "add-feed endpoint not found in relay API"))))))))
(defun org-social-relay--fetch-feeds (callback)
"Fetch list of feeds from the relay server.
Call CALLBACK with the list of feed URLs."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((feeds-endpoint (gethash "feeds" endpoints)))
(if feeds-endpoint
(let ((href (plist-get feeds-endpoint :href))
(method (plist-get feeds-endpoint :method)))
(request (concat relay-url href)
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(feeds-data (cdr (assoc 'data response))))
;; Handle both vectors and lists
(let ((feeds-list (if (vectorp feeds-data)
(append feeds-data nil)
feeds-data)))
(funcall callback feeds-list)))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay feeds response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch feeds from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil)))))
(message "feeds endpoint not found in relay API")
(funcall callback nil))))))))
(defun org-social-relay--fetch-notifications (callback &optional type)
"Fetch notifications for the user's feed from the relay server.
Call CALLBACK with the list of notification objects
\(mentions, reactions, replies).
Optional TYPE filters by notification type:
\\='mention, \\='reaction, or \\='reply."
(when (and org-social-relay
org-social-my-public-url
(not (string-empty-p org-social-relay))
(not (string-empty-p org-social-my-public-url)))
(let ((relay-url (string-trim-right org-social-relay "/"))
(feed-url org-social-my-public-url))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((notifications-endpoint (gethash "notifications" endpoints)))
(if notifications-endpoint
(let ((href (plist-get notifications-endpoint :href))
(method (plist-get notifications-endpoint :method)))
(let ((url (if type
(concat relay-url
(replace-regexp-in-string
"{\\(feed_url\\|url feed\\)}"
(url-hexify-string feed-url)
href)
(format "&type=%s" (symbol-name type)))
(concat relay-url
(replace-regexp-in-string
"{\\(feed_url\\|url feed\\)}"
(url-hexify-string feed-url)
href)))))
(request url
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(notifications-data (cdr (assoc 'data response))))
(if (string= response-type "Success")
(let ((notifications-list (if (vectorp notifications-data)
(append notifications-data nil)
notifications-data)))
(funcall callback notifications-list))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay notifications response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch notifications from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil))))))
(message "notifications endpoint not found in relay API")
(funcall callback nil))))))))
(defun org-social-relay--fetch-mentions (callback)
"Fetch mentions for the user's feed from the relay server.
Call CALLBACK with the list of post URLs that mention the user."
(when (and org-social-relay
org-social-my-public-url
(not (string-empty-p org-social-relay))
(not (string-empty-p org-social-my-public-url)))
(let ((relay-url (string-trim-right org-social-relay "/"))
(feed-url org-social-my-public-url))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((mentions-endpoint (gethash "mentions" endpoints)))
(if mentions-endpoint
(let ((href (plist-get mentions-endpoint :href))
(method (plist-get mentions-endpoint :method)))
;; Replace {feed_url} or {url feed} placeholder with actual feed URL
(let ((url (concat relay-url
(replace-regexp-in-string
"{\\(feed_url\\|url feed\\)}"
(url-hexify-string feed-url)
href))))
(request url
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(mentions-data (cdr (assoc 'data response))))
;; Check if response is successful
(if (string= response-type "Success")
(progn
;; Handle both vectors and lists
(let ((mentions-list (if (vectorp mentions-data)
(append mentions-data nil)
mentions-data)))
(funcall callback mentions-list)))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay mentions response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch mentions from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil))))))
(message "mentions endpoint not found in relay API")
(funcall callback nil))))))))
(defun org-social-relay--fetch-replies (post-url callback)
"Fetch replies for POST-URL from the relay server.
Call CALLBACK with the thread structure."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((replies-endpoint (gethash "replies" endpoints)))
(if replies-endpoint
(let ((href (plist-get replies-endpoint :href))
(method (plist-get replies-endpoint :method)))
(let ((url (concat relay-url
(replace-regexp-in-string
"{\\(post_url\\|url post\\)}"
(url-hexify-string post-url)
href))))
(request url
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(replies-data (cdr (assoc 'data response))))
(if (string= response-type "Success")
(let ((replies-list (if (vectorp replies-data)
(append replies-data nil)
replies-data)))
(funcall callback replies-list))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay replies response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch replies from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil))))))
(message "replies endpoint not found in relay API")
(funcall callback nil))))))))
(defun org-social-relay--fetch-interactions (post-url callback)
"Fetch all interactions (reactions, replies, boosts) for POST-URL.
Fetches from the relay server.
Call CALLBACK with the interactions data structure."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((interactions-endpoint (gethash "interactions" endpoints)))
(if interactions-endpoint
(let ((href (plist-get interactions-endpoint :href))
(method (plist-get interactions-endpoint :method)))
(let ((url (concat relay-url
(replace-regexp-in-string
"{\\(post_url\\|url post\\|post\\)}"
(url-hexify-string post-url)
href))))
(request url
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(interactions-data (cdr (assoc 'data response))))
(if (string= response-type "Success")
(funcall callback interactions-data)
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay interactions response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch interactions from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil))))))
(message "interactions endpoint not found in relay API")
(funcall callback nil))))))))
(defun org-social-relay--search-posts (query callback &optional search-type page per-page)
"Search posts by QUERY from the relay server.
SEARCH-TYPE can be \\='tag to search by tag, defaults to text search.
PAGE is the page number (default 1).
PER-PAGE is results per page (default 10, max 50).
Call CALLBACK with the list of matching post URLs and metadata."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((search-endpoint (gethash "search" endpoints)))
(if search-endpoint
(let ((href (plist-get search-endpoint :href))
(method (plist-get search-endpoint :method)))
;; Replace {query} placeholder in href with actual query
(let* ((base-url (concat relay-url
(replace-regexp-in-string
"{query}"
(url-hexify-string query)
;; Use correct parameter name based on search type
(replace-regexp-in-string
"\\?q="
(if (eq search-type 'tag) "?tag=" "?q=")
href))))
;; Add pagination parameters
(url (concat base-url
(when page (format "&page=%d" page))
(when per-page (format "&perPage=%d" per-page)))))
(request url
:type method
:timeout 15
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(search-data (cdr (assoc 'data response)))
(meta-data (cdr (assoc 'meta response))))
(if (string= response-type "Success")
(let ((search-list (if (vectorp search-data)
(append search-data nil)
search-data)))
;; Call callback with both data and metadata
(funcall callback search-list meta-data))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil nil)))
(error
(message "Failed to parse relay search response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to search posts from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil nil))))))
(message "search endpoint not found in relay API")
(funcall callback nil nil))))))))
(defun org-social-relay--fetch-groups-from-url (relay-url callback)
"Fetch list of groups from RELAY-URL.
Call CALLBACK with a list of alists, each containing (name . NAME),
\(href . HREF), (method . METHOD), and (relay-url . RELAY-URL).
NAME is the display name of the group (e.g., \\='Org Mode\\=').
HREF is the relay endpoint for the group (e.g., \\='/groups/org-mode/\\=').
METHOD is the HTTP method to use (e.g., \\='GET\\=').
RELAY-URL is the base URL of the relay server."
(when (and relay-url (not (string-empty-p relay-url)))
(let ((relay-url (string-trim-right relay-url "/")))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((groups-endpoint (gethash "groups" endpoints)))
(if groups-endpoint
(let ((href (plist-get groups-endpoint :href))
(method (plist-get groups-endpoint :method)))
(request (concat relay-url href)
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(links (cdr (assoc '_links response)))
(groups-links (cdr (assoc 'groups links))))
(if (string= response-type "Success")
;; Extract name and href from _links.groups
(let* ((groups-array (if (vectorp groups-links)
(append groups-links nil)
groups-links))
(groups-list
(mapcar (lambda (group-obj)
(let ((name (cdr (assoc 'name group-obj)))
(href (cdr (assoc 'href group-obj)))
(method (cdr (assoc 'method group-obj))))
`((name . ,name)
(href . ,href)
(method . ,method)
(relay-url . ,relay-url))))
groups-array)))
(funcall callback groups-list))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay groups response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch groups from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil)))))
(message "groups endpoint not found in relay API")
(funcall callback nil))))))))
(defun org-social-relay--fetch-groups (callback)
"Fetch list of groups from the default relay server (org-social-relay).
Call CALLBACK with a list of alists, each containing (name . NAME),
\(href . HREF), (method . METHOD), and (relay-url . RELAY-URL).
This is a wrapper around `org-social-relay--fetch-groups-from-url\\=`
for backward compatibility."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(org-social-relay--fetch-groups-from-url org-social-relay callback)))
(defun org-social-relay--fetch-group-posts (group-href group-method callback)
"Fetch posts from GROUP-HREF from the relay server.
GROUP-HREF is the endpoint path for the group (e.g., \\='/groups/org-mode/\\=').
GROUP-METHOD is the HTTP method to use (e.g., \\='GET\\=').
Call CALLBACK with the list of group posts."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(request (concat relay-url group-href)
:type group-method
:timeout 15
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(posts-data (cdr (assoc 'data response))))
(if (string= response-type "Success")
(let ((posts-list (if (vectorp posts-data)
(append posts-data nil)
posts-data)))
(funcall callback posts-list))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay group posts response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch group posts from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil)))))))
(defun org-social-relay--fetch-polls (callback)
"Fetch polls from the relay server.
Call CALLBACK with the list of poll URLs."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((polls-endpoint (gethash "polls" endpoints)))
(if polls-endpoint
(let ((href (plist-get polls-endpoint :href))
(method (plist-get polls-endpoint :method)))
(request (concat relay-url href)
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(polls-data (cdr (assoc 'data response))))
(if (string= response-type "Success")
(let ((polls-list (if (vectorp polls-data)
(append polls-data nil)
polls-data)))
(funcall callback polls-list))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay polls response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch polls from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil)))))
(message "polls endpoint not found in relay API")
(funcall callback nil))))))))
(defun org-social-relay--fetch-poll-votes (post-url callback)
"Fetch votes for poll at POST-URL from the relay server.
Call CALLBACK with the poll votes data."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((poll-votes-endpoint (gethash "poll-votes" endpoints)))
(if poll-votes-endpoint
(let ((href (plist-get poll-votes-endpoint :href))
(method (plist-get poll-votes-endpoint :method)))
(let ((url (concat relay-url
(replace-regexp-in-string
"{\\(post_url\\|url post\\)}"
(url-hexify-string post-url)
href))))
(request url
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(votes-data (cdr (assoc 'data response))))
(if (string= response-type "Success")
(let ((votes-list (if (vectorp votes-data)
(append votes-data nil)
votes-data)))
(funcall callback votes-list))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay poll votes response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch poll votes from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil))))))
(message "poll-votes endpoint not found in relay API")
(funcall callback nil))))))))
;; Utility functions for getting data synchronously (for compatibility)
(defun org-social-relay--get-timeline ()
"Get timeline from relay (fallback to local feeds if relay unavailable)."
(if (and org-social-relay (not (string-empty-p org-social-relay)))
;; For now, use feed-based approach until we implement full timeline endpoint
(when (fboundp 'org-social-feed--get-timeline)
(org-social-feed--get-timeline))
;; Fallback to local feeds
(when (fboundp 'org-social-feed--get-timeline)
(org-social-feed--get-timeline))))
(defun org-social-relay--get-thread (post-url)
"Get thread for POST-URL from relay."
;; This will be implemented when we create the UI
(message "Getting thread for %s" post-url))
(defun org-social-relay--get-mentions (feed-url)
"Get mentions for FEED-URL from relay."
;; This will be implemented when we create the UI
(message "Getting mentions for %s" feed-url))
(defun org-social-relay--get-groups ()
"Get groups from relay."
;; This will be implemented when we create the UI
(message "Getting groups from relay"))
(defun org-social-relay--get-group-posts (group-name)
"Get posts for GROUP-NAME from relay."
;; This will be implemented when we create the UI
(message "Getting posts for group %s" group-name))
(defun org-social-relay--check-posts-for-replies (post-urls callback)
"Check multiple POST-URLS for replies and call CALLBACK with results.
CALLBACK receives an alist where each entry is (post-url . has-replies).
Designed for small batches of posts (e.g., current page)."
(when (and org-social-relay
(not (string-empty-p org-social-relay))
post-urls)
(let ((relay-url (string-trim-right org-social-relay "/"))
(results '())
(completed 0)
(total (length post-urls)))
(org-social-relay--discover-endpoints
relay-url
(lambda (endpoints)
(let ((replies-endpoint (gethash "replies" endpoints)))
(if replies-endpoint
(let ((href (plist-get replies-endpoint :href))
(method (plist-get replies-endpoint :method)))
(dolist (post-url post-urls)
(let ((url (concat relay-url
(replace-regexp-in-string
"{\\(post_url\\|url post\\)}"
(url-hexify-string post-url)
href))))
(request url
:type method
:timeout 10
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case _err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(replies-data (cdr (assoc 'data response))))
(let ((has-replies
(and (string= response-type "Success")
replies-data
(or (and (listp replies-data) (> (length replies-data) 0))
(and (vectorp replies-data) (> (length replies-data) 0))))))
(push (cons post-url has-replies) results)
(setq completed (1+ completed))
(when (= completed total)
(funcall callback results))))
(progn
(push (cons post-url nil) results)
(setq completed (1+ completed))
(when (= completed total)
(funcall callback results))))
(error
(push (cons post-url nil) results)
(setq completed (1+ completed))
(when (= completed total)
(funcall callback results))))))
:error (cl-function
(lambda (&key &allow-other-keys)
(push (cons post-url nil) results)
(setq completed (1+ completed))
(when (= completed total)
(funcall callback results))))))))
(progn
(message "replies endpoint not found in relay API")
(funcall callback nil)))))))))
(defun org-social-relay--check-post-has-replies (post-url callback)
"Check if POST-URL has replies from the relay server.
Call CALLBACK with t if post has replies, nil otherwise."
(org-social-relay--check-posts-for-replies
(list post-url)
(lambda (results)
(let ((result (alist-get post-url results nil nil 'string=)))
(funcall callback result)))))
(defun org-social-relay--fetch-group-details (group-href group-method callback)
"Fetch complete group details including posts and metadata from GROUP-HREF.
GROUP-HREF is the endpoint path for the group (e.g., \\='/groups/org-mode/\\=').
GROUP-METHOD is the HTTP method to use (e.g., \\='GET\\=').
Call CALLBACK with an alist containing \\='posts and \\='members keys."
(when (and org-social-relay
(not (string-empty-p org-social-relay)))
(let ((relay-url (string-trim-right org-social-relay "/")))
(request (concat relay-url group-href)
:type group-method
:timeout 15
:success (cl-function
(lambda (&key data &allow-other-keys)
(condition-case err
(if (and data (stringp data) (not (string-empty-p data)))
(let* ((response (json-read-from-string data))
(response-type (cdr (assoc 'type response)))
(posts-data (cdr (assoc 'data response)))
(meta-data (cdr (assoc 'meta response))))
(if (string= response-type "Success")
(let ((posts-list (if (vectorp posts-data)
(append posts-data nil)
posts-data))
(members-list (if meta-data
(let ((members (cdr (assoc 'members meta-data))))
(if (vectorp members)
(append members nil)
members))
'())))
(funcall callback `((posts . ,posts-list)
(members . ,members-list))))
(progn
(message "Relay returned error response: %s" response-type)
(funcall callback nil))))
(progn
(message "Received empty, nil, or non-string response from relay: %S" data)
(funcall callback nil)))
(error
(message "Failed to parse relay group details response: %s (data: %S)" (error-message-string err) data)
(funcall callback nil)))))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Failed to fetch group details from relay: %s"
(if error-thrown
(error-message-string error-thrown)
"Unknown error"))
(funcall callback nil)))))))
(provide 'org-social-relay)
;;; org-social-relay.el ends here