Skip to content

Commit

Permalink
Fix: Persist session data for reconnect
Browse files Browse the repository at this point in the history
When disconnecting from a session, we now keep the slots necessary for
reconnecting, and we persist those slots.

Previously all of the session's data was discarded after writing, so
if multiple accounts were disconnected but not at the same time, the
first session to be disconnected was forgotten.

Fixes #243.

Reported-by: formula-spectre <https://github.com/formula-spectre>
  • Loading branch information
alphapapa committed Oct 13, 2024
1 parent e78ad47 commit 7f6032a
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 61 deletions.
70 changes: 36 additions & 34 deletions ement-directory.el
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ Show up to LIMIT rooms. Interactively, with prefix, prompt for
server and LIMIT.
SINCE may be a next-batch token."
(interactive (let* ((session (ement-complete-session :prompt "Search on session: "))
(interactive (let* ((session (ement-complete-session :prompt "Search on session: "
:pred #'ement-session-has-synced-p))
(server (if current-prefix-arg
(read-string "Search on server: " nil nil
(ement-server-name (ement-session-server session)))
Expand All @@ -203,31 +204,32 @@ SINCE may be a next-batch token."
('total_room_count_estimate remaining))
results))
(ement-directory--view rooms :append-p since
:buffer-name (format "*Ement Directory: %s*" server)
:root-section-name (format "Ement Directory: %s" server)
:init-fn (lambda ()
(setf (alist-get 'server ement-directory-etc) server
(alist-get 'session ement-directory-etc) session
(alist-get 'next-batch ement-directory-etc) next-batch
(alist-get 'limit ement-directory-etc) limit)
(setq-local revert-buffer-function revert-function)
(when remaining
;; FIXME: The server seems to report all of the rooms on
;; the server as remaining even when searching for a
;; specific term like "emacs".
;; TODO: Display this in a more permanent place (like a
;; header or footer).
(message
(substitute-command-keys
"%s rooms remaining (use \\[ement-directory-next] to fetch more)")
remaining)))))))
:buffer-name (format "*Ement Directory: %s*" server)
:root-section-name (format "Ement Directory: %s" server)
:init-fn (lambda ()
(setf (alist-get 'server ement-directory-etc) server
(alist-get 'session ement-directory-etc) session
(alist-get 'next-batch ement-directory-etc) next-batch
(alist-get 'limit ement-directory-etc) limit)
(setq-local revert-buffer-function revert-function)
(when remaining
;; FIXME: The server seems to report all of the rooms on
;; the server as remaining even when searching for a
;; specific term like "emacs".
;; TODO: Display this in a more permanent place (like a
;; header or footer).
(message
(substitute-command-keys
"%s rooms remaining (use \\[ement-directory-next] to fetch more)")
remaining)))))))
(ement-message "Listing %s rooms on %s..." limit server)))

;;;###autoload
(cl-defun ement-directory-search (query &key server session since (limit 1000))
"View public rooms on SERVER matching QUERY.
QUERY is a string used to filter results."
(interactive (let* ((session (ement-complete-session :prompt "Search on session: "))
(interactive (let* ((session (ement-complete-session :prompt "Search on session: "
:pred #'ement-session-has-synced-p))
(server (if current-prefix-arg
(read-string "Search on server: " nil nil
(ement-server-name (ement-session-server session)))
Expand All @@ -253,20 +255,20 @@ QUERY is a string used to filter results."
('total_room_count_estimate remaining))
results))
(ement-directory--view rooms :append-p since
:buffer-name (format "*Ement Directory: \"%s\" on %s*" query server)
:root-section-name (format "Ement Directory: \"%s\" on %s" query server)
:init-fn (lambda ()
(setf (alist-get 'server ement-directory-etc) server
(alist-get 'session ement-directory-etc) session
(alist-get 'next-batch ement-directory-etc) next-batch
(alist-get 'limit ement-directory-etc) limit
(alist-get 'query ement-directory-etc) query)
(setq-local revert-buffer-function revert-function)
(when remaining
(message
(substitute-command-keys
"%s rooms remaining (use \\[ement-directory-next] to fetch more)")
remaining)))))))
:buffer-name (format "*Ement Directory: \"%s\" on %s*" query server)
:root-section-name (format "Ement Directory: \"%s\" on %s" query server)
:init-fn (lambda ()
(setf (alist-get 'server ement-directory-etc) server
(alist-get 'session ement-directory-etc) session
(alist-get 'next-batch ement-directory-etc) next-batch
(alist-get 'limit ement-directory-etc) limit
(alist-get 'query ement-directory-etc) query)
(setq-local revert-buffer-function revert-function)
(when remaining
(message
(substitute-command-keys
"%s rooms remaining (use \\[ement-directory-next] to fetch more)")
remaining)))))))
(ement-message "Searching for %S on %s..." query server)))

(defun ement-directory-next ()
Expand Down
33 changes: 21 additions & 12 deletions ement-lib.el
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ include with the request (see Matrix spec)."
;; TODO: Document other arguments.
;; SPEC: 10.1.1.
(declare (indent defun))
(interactive (list (ement-complete-session)
(interactive (list (ement-complete-session :prompt "Make room as: "
:pred #'ement-session-has-synced-p)
:name (read-string "New room name: ")
:alias (read-string "New room alias (e.g. \"foo\" for \"#foo:matrix.org\"): ")
:topic (read-string "New room topic: ")
Expand Down Expand Up @@ -200,7 +201,8 @@ include with the request (see Matrix spec)."
Then call function THEN with response data. Optional string
arguments are NAME, ALIAS, and TOPIC."
(declare (indent defun))
(interactive (list (ement-complete-session)
(interactive (list (ement-complete-session :prompt "Make space as: "
:pred #'ement-session-has-synced-p)
:name (read-string "New space name: ")
:alias (read-string "New space alias (e.g. \"foo\" for \"#foo:matrix.org\"): ")
:topic (read-string "New space topic: ")
Expand Down Expand Up @@ -292,7 +294,8 @@ when necessary, and forget the room without prompting."
"Ignore USER-ID on SESSION.
If UNIGNORE-P (interactively, with prefix), un-ignore USER."
(interactive (list (ement-complete-user-id)
(ement-complete-session)
(ement-complete-session :prompt "Ignore user as: "
:pred #'ement-session-has-synced-p)
current-prefix-arg))
(pcase-let* (((cl-struct ement-session account-data) session)
;; TODO: Store session account-data events in an alist keyed on type.
Expand Down Expand Up @@ -376,7 +379,8 @@ Uses the latest existing direct room with the user, or creates a
new one automatically if necessary."
;; SPEC: 13.23.2.
(interactive
(let* ((session (ement-complete-session))
(let* ((session (ement-complete-session :prompt "Send as: "
:pred #'ement-session-has-synced-p))
(user-id (ement-complete-user-id))
(message (read-string "Message: ")))
(list session user-id message)))
Expand Down Expand Up @@ -449,7 +453,8 @@ new one automatically if necessary."
"Set DISPLAY-NAME for user on SESSION.
Sets global displayname."
(interactive
(let* ((session (ement-complete-session))
(let* ((session (ement-complete-session :prompt "Set name for: "
:pred #'ement-session-has-synced-p))
(display-name (read-string "Set display-name to: " nil nil
(ement-user-displayname (ement-session-user session)))))
(list display-name session)))
Expand Down Expand Up @@ -772,14 +777,18 @@ THEN and ELSE are passed to `ement-api', which see."
:content-type content-type :data data :data-type 'binary
:then then :else else))

(cl-defun ement-complete-session (&key (prompt "Session: "))
"Return an Ement session selected with completion."
(cl-defun ement-complete-session (&key (pred #'identity) (prompt "Session: "))
"Return an Ement session selected with completion.
PROMPT is passed to `completing-read'. Only offer sessions
matching PRED."
(pcase (length ement-sessions)
(0 (user-error "No active sessions. Call `ement-connect' to log in"))
(1 (cdar ement-sessions))
(_ (let* ((ids (mapcar #'car ement-sessions))
(selected-id (completing-read prompt ids nil t)))
(alist-get selected-id ement-sessions nil nil #'equal)))))
(0 (user-error "No sessions. Call `ement-connect' to log in"))
(_ (let ((sessions (cl-remove-if-not pred ement-sessions :key #'cdr)))
(pcase (length sessions)
(1 (cdar sessions))
(_ (let* ((ids (mapcar #'car sessions))
(selected-id (completing-read prompt ids nil t)))
(alist-get selected-id sessions nil nil #'equal))))))))

(declare-function ewoc-locate "ewoc")
(defun ement-complete-user-id ()
Expand Down
10 changes: 7 additions & 3 deletions ement-notifications.el
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ LIMIT may be a maximum number of events to return. ONLY may be
the string \"highlight\" to only return notifications that have
the highlight tweak set. THEN and ELSE may be callbacks passed
to `ement-api', which see."
(interactive (list (ement-complete-session)
(interactive (list (ement-complete-session :prompt "Show notifications for: "
:pred #'ement-session-has-synced-p)
:only (when current-prefix-arg
"highlight")))
(if-let ((buffer (get-buffer "*Ement Notifications*")))
Expand Down Expand Up @@ -170,7 +171,8 @@ to `ement-api', which see."
;; FIXME: Naming things is hard.
"Retrieve NUMBER older notifications on SESSION."
;; FIXME: Support multiple sessions.
(interactive (list (ement-complete-session)
(interactive (list (ement-complete-session :prompt "Retrieve notifications for: "
:pred #'ement-session-has-synced-p)
(cl-typecase current-prefix-arg
(null 100)
(list (read-number "Number of messages: "))
Expand Down Expand Up @@ -292,7 +294,9 @@ to `ement-api', which see."
;; the command is asynchronous in that case, so the buffer can be displayed in the wrong
;; window. Fixing this would be hacky and awkward, but a partial solution is probably
;; possible.
(ement-notifications (ement-complete-session)))
(ement-notifications
(ement-complete-session :prompt "Show notifications for: "
:pred #'ement-session-has-synced-p)))

;;; Footer

Expand Down
9 changes: 7 additions & 2 deletions ement-room.el
Original file line number Diff line number Diff line change
Expand Up @@ -1775,7 +1775,8 @@ THEN may be a function to call after joining the room (and when
buffer). It receives two arguments, the room and the session."
(interactive (list (read-string "Join room (ID or alias): ")
(or ement-session
(ement-complete-session))))
(ement-complete-session :prompt "Join as: "
:pred #'ement-session-has-synced-p))))
(cl-assert id-or-alias) (cl-assert session)
(unless (string-match-p
;; According to tulir in #matrix-dev:matrix.org, ": is not
Expand Down Expand Up @@ -2426,7 +2427,11 @@ these all require at least version 29 of Emacs):
(defun ement-room-view (room session)
"Switch to a buffer showing ROOM on SESSION.
Uses action `ement-view-room-display-buffer-action', which see."
(interactive (ement-complete-room :session (ement-complete-session) :suggest nil
(interactive (ement-complete-room
:session (ement-complete-session
:prompt "View as: "
:pred #'ement-session-has-synced-p)
:suggest nil
:predicate (lambda (room)
(not (ement--space-p room)))))
(pcase-let* (((cl-struct ement-room (local (map buffer))) room))
Expand Down
46 changes: 36 additions & 10 deletions ement.el
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,10 @@ the port, e.g.
(cl-case (length ement-sessions)
(0 (list :user-id (read-string "User ID: " nil 'ement-connect-user-id-history)))
(1 (list :session (cdar ement-sessions)))
(otherwise (list :session (ement-complete-session))))))
(otherwise (list :session (ement-complete-session
:prompt "Connect as user ID: "
:pred (lambda (session)
(not (ement-session-has-synced-p session)))))))))
(let (sso-server-process)
(cl-labels ((new-session ()
(unless (string-match (rx bos "@" (group (1+ (not (any ":")))) ; Username
Expand Down Expand Up @@ -329,6 +332,9 @@ Ement: SSO login accepted; session token received. Connecting to Matrix server.
(if session
;; Start syncing given session.
(let ((user-id (ement-user-id (ement-session-user session))))
(unless (hash-table-p (ement-session-events session))
;; HACK: Ensure session's events slot is a hash table (when disconnecting, it's cleared).
(setf (ement-session-events session) (make-hash-table :test #'equal)))
;; HACK: If session is already in ement-sessions, this replaces it. I think that's okay...
(setf (alist-get user-id ement-sessions nil nil #'equal) session)
(ement--sync session :timeout ement-initial-sync-timeout))
Expand All @@ -349,10 +355,8 @@ room buffers are left alive and can be read, but other commands
in them won't work."
(interactive (list (if current-prefix-arg
(mapcar #'cdr ement-sessions)
(list (ement-complete-session)))))
(when ement-save-sessions
;; Write sessions before we remove them from the variable.
(ement--write-sessions ement-sessions))
(list (ement-complete-session :prompt "Disconnect: "
:pred #'ement-session-has-synced-p)))))
(dolist (session sessions)
(let ((user-id (ement-user-id (ement-session-user session))))
(when-let ((process (map-elt ement-syncs session)))
Expand All @@ -362,11 +366,16 @@ in them won't work."
(delete-process process))
;; NOTE: I'd like to use `map-elt' here, but not until
;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=47368> is fixed, I guess.
(setf (alist-get session ement-syncs nil nil #'equal) nil
(alist-get user-id ement-sessions nil 'remove #'equal) nil)))
(unless ement-sessions
;; HACK: If no sessions remain, clear the users table. It might be best
;; to store a per-session users table, but this is probably good enough.
(setf (alist-get session ement-syncs) nil
;; Set the session to an "emptied" one, which only retains slots needed to
;; reconnect.
(alist-get user-id ement-sessions nil nil #'equal) (ement--emptied session))))
(when ement-save-sessions
;; Write sessions now that we have emptied the session.
(ement--write-sessions ement-sessions))
(unless (cl-find-if #'ement-session-has-synced-p ement-sessions :key #'cdr)
;; HACK: If no connected sessions remain, clear the users table. It might be best to
;; store a per-session users table, but this is probably good enough.
(clrhash ement-users))
;; TODO: Should call this hook for each session with the session as argument.
(run-hooks 'ement-disconnect-hook)
Expand Down Expand Up @@ -873,6 +882,7 @@ Returns nil if unable to read `ement-sessions-file'."
:token token
:transaction-id transaction-id))))
(message "Ement: Writing sessions...")
;; TODO: Use `persist'.
(with-temp-file ement-sessions-file
(pcase-let* ((print-level nil)
(print-length nil)
Expand All @@ -885,6 +895,22 @@ Returns nil if unable to read `ement-sessions-file'."
;; Ensure permissions are safe.
(chmod ement-sessions-file #o600)))

(cl-defmethod ement--emptied ((session ement-session))
"Return a copy of SESSION with most slots nulled.
The copy only includes these slots: user, server, token, and
transaction-id. This is suitable for persisting upon disconnect
so the session can be reconnected, but allowing most data to be
garbage-collected."
(pcase-let* (((cl-struct ement-session user server token transaction-id) session)
((cl-struct ement-user (id user-id) username) user)
((cl-struct ement-server (name server-name) uri-prefix) server))
(make-ement-session :user (make-ement-user :id user-id
:username username)
:server (make-ement-server :name server-name
:uri-prefix uri-prefix)
:token token
:transaction-id transaction-id)))

(defun ement--kill-emacs-hook ()
"Function to be added to `kill-emacs-hook'.
Writes Ement session to disk when enabled."
Expand Down

0 comments on commit 7f6032a

Please sign in to comment.