Handle chats being modified; allow the user to refresh all groups

- We now handle the :chat-modified event from whatscl, and re-fetch group
  metadata when we receive it.
- Of course, this doesn't actually help for resources which are in the MUC at
  update time; they'll need to reconnect until we have code to handle message
  stubs properly.
- Chat metadata reception now generates a message to the user, both for ease of
  debugging, and also to let them know that something happened to a groupchat
  they're in which they would otherwise not be aware of at all. (A handy link is
  also provided, if you missed the invite and want to join a MUC you're not in.)
- The new `refresh-chats` admin command forces a refresh of all group metadata
  in one big go. (Also, the help text now displays the current version.)
This commit is contained in:
eta 2020-08-02 12:59:59 +01:00
parent b47828c5c1
commit a02c0fdc79
2 changed files with 41 additions and 14 deletions

View file

@ -94,7 +94,7 @@
(defun insert-user-chat (uid wa-id) (defun insert-user-chat (uid wa-id)
"Inserts a user chat with localpart WA-ID into the database for the user with UID." "Inserts a user chat with localpart WA-ID into the database for the user with UID."
(with-prepared-statements (with-prepared-statements
((insert-stmt "INSERT INTO user_chats (user_id, wa_jid) VALUES (?, ?)")) ((insert-stmt "INSERT INTO user_chats (user_id, wa_jid) VALUES (?, ?) ON CONFLICT DO NOTHING"))
(bind-parameters insert-stmt uid wa-id) (bind-parameters insert-stmt uid wa-id)
(sqlite:step-statement insert-stmt))) (sqlite:step-statement insert-stmt)))

View file

@ -89,14 +89,17 @@
(concatenate 'string "admin@" (component-name comp) "/adminbot")) (concatenate 'string "admin@" (component-name comp) "/adminbot"))
(defparameter *admin-help-text* (defparameter *admin-help-text*
"This is a very beta WhatsApp to XMPP bridge! (format nil
"** whatsxmpp, version ~A, a theta.eu.org project **
Commands: Commands:
- register: set up the bridge - register: set up the bridge
- connect: manually connect to WhatsApp - connect: manually connect to WhatsApp
- stop: disconnect from WhatsApp, and disable automatic reconnections - stop: disconnect from WhatsApp, and disable automatic reconnections
- status: get your current status - status: get your current status
- getroster: trigger an XEP-0144 roster item exchange (in some clients, this'll pop up a window asking to add contacts to your roster) - getroster: trigger an XEP-0144 roster item exchange (in some clients, this'll pop up a window asking to add contacts to your roster)
- help: view this help text") - help: view this help text
- refresh-chats: force the bridge to update member lists + subject for all of your group chats"
+version+))
(defparameter *reconnect-every-secs* 5 (defparameter *reconnect-every-secs* 5
"Interval between calls to WA-RESETUP-USERS.") "Interval between calls to WA-RESETUP-USERS.")
@ -634,6 +637,13 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
(format *debug-io* "~&got contact ~A for ~A~%" contact jid) (format *debug-io* "~&got contact ~A for ~A~%" contact jid)
(add-wa-contact comp conn jid contact))) (add-wa-contact comp conn jid contact)))
(defun request-wa-chat-metadata (comp conn jid wx-localpart)
"Request chat metadata for WX-LOCALPART (a MUC localpart) for the user with JID."
(format *debug-io* "~&requesting chat metadata for ~A from ~A~%" wx-localpart jid)
(whatscl::get-group-metadata conn (whatsxmpp-localpart-to-wa-jid wx-localpart)
(lambda (conn meta)
(wa-handle-group-metadata comp conn jid wx-localpart meta))))
(defun handle-wa-chat-invitation (comp conn jid uid localpart &key noretry) (defun handle-wa-chat-invitation (comp conn jid uid localpart &key noretry)
"Checks to see whether the group chat LOCALPART has any metadata; if not, requests some. If it does, and the user hasn't been invited to that group chat yet, send them an invitation." "Checks to see whether the group chat LOCALPART has any metadata; if not, requests some. If it does, and the user hasn't been invited to that group chat yet, send them an invitation."
(unless (uiop:string-prefix-p "g" localpart) (unless (uiop:string-prefix-p "g" localpart)
@ -661,12 +671,7 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
(bind-parameters update-stmt "invited" chat-id) (bind-parameters update-stmt "invited" chat-id)
(sqlite:step-statement update-stmt)) (sqlite:step-statement update-stmt))
(unless noretry (unless noretry
(format *debug-io* "~&requesting chat metadata for ~A from ~A~%" localpart jid) (request-wa-chat-metadata comp conn jid localpart)))))))
(whatscl::get-group-metadata conn (whatsxmpp-localpart-to-wa-jid localpart)
(lambda (conn meta)
(wa-handle-group-metadata comp conn jid localpart meta)))))))))
(defun add-wa-chat (comp conn jid ct-jid) (defun add-wa-chat (comp conn jid ct-jid)
"Adds the JID CT-JID to the list of the user's groupchats, if it is a groupchat." "Adds the JID CT-JID to the list of the user's groupchats, if it is a groupchat."
@ -827,9 +832,13 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
(defun wa-handle-group-metadata (comp conn jid localpart data) (defun wa-handle-group-metadata (comp conn jid localpart data)
(with-wa-handler-context (comp conn jid) (with-wa-handler-context (comp conn jid)
(let* ((uid (get-user-id jid)) (let* ((uid (get-user-id jid))
(cid (get-user-chat-id uid localpart))) (cid (get-user-chat-id uid localpart))
(subject (whatscl::cassoc :subject data)))
(unless cid
(setf cid (insert-user-chat uid localpart)))
(format *debug-io* "~&got group metadata for ~A from ~A~%" localpart jid) (format *debug-io* "~&got group metadata for ~A from ~A~%" localpart jid)
(unless (whatscl::cassoc :subject data) (unless subject
(admin-msg comp jid (format nil "Warning: Failed to update group ~A: received ~A~%This warning usually appears when trying to get information for a group you're no longer in, and can be safely ignored." localpart data))
(warn "Received incomplete group metadata for ~A from ~A: ~A" localpart jid data) (warn "Received incomplete group metadata for ~A from ~A: ~A" localpart jid data)
(return-from wa-handle-group-metadata)) (return-from wa-handle-group-metadata))
(when cid (when cid
@ -838,8 +847,7 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
(delete-members-stmt "DELETE FROM user_chat_members WHERE chat_id = ?") (delete-members-stmt "DELETE FROM user_chat_members WHERE chat_id = ?")
(insert-member-stmt "INSERT INTO user_chat_members (chat_id, wa_jid, resource, affiliation) VALUES (?, ?, ?, ?)")) (insert-member-stmt "INSERT INTO user_chat_members (chat_id, wa_jid, resource, affiliation) VALUES (?, ?, ?, ?)"))
(with-transaction (with-transaction
(let ((subject (whatscl::cassoc :subject data))) (bind-parameters update-subject-stmt subject cid)
(bind-parameters update-subject-stmt subject cid))
(sqlite:step-statement update-subject-stmt) (sqlite:step-statement update-subject-stmt)
(sqlite:step-statement delete-members-stmt) (sqlite:step-statement delete-members-stmt)
(loop (loop
@ -859,6 +867,7 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
"member")))) "member"))))
(sqlite:step-statement insert-member-stmt) (sqlite:step-statement insert-member-stmt)
(sqlite:reset-statement insert-member-stmt))) (sqlite:reset-statement insert-member-stmt)))
(admin-msg comp jid (format nil "New or updated WhatsApp group chat: \"~A\" (xmpp:~A@~A?join)" subject localpart (component-name comp)))
(handle-wa-chat-invitation comp conn jid uid localpart :noretry t)))))) (handle-wa-chat-invitation comp conn jid uid localpart :noretry t))))))
(defun wa-handle-presence (comp conn jid &key for-jid type participant &allow-other-keys) (defun wa-handle-presence (comp conn jid &key for-jid type participant &allow-other-keys)
@ -880,12 +889,19 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
(cxml:with-element chat-state (cxml:with-element chat-state
(cxml:attribute "xmlns" +chat-states-ns+)))))))) (cxml:attribute "xmlns" +chat-states-ns+))))))))
(defun wa-handle-chat-modified (comp conn jid chat-jid)
(with-wa-handler-context (comp conn jid)
(let ((wx-localpart (wa-jid-to-whatsxmpp-localpart chat-jid)))
(format *debug-io* "~&chat-modified: ~A for ~A~&" wx-localpart jid)
(request-wa-chat-metadata comp conn jid wx-localpart))))
(defun bind-wa-handlers (comp conn jid) (defun bind-wa-handlers (comp conn jid)
(on :ws-close conn (lambda (&rest args) (on :ws-close conn (lambda (&rest args)
(declare (ignore args)) (declare (ignore args))
(wa-handle-ws-close comp conn jid))) (wa-handle-ws-close comp conn jid)))
(on :ws-error conn (lambda (e) (wa-handle-ws-error comp conn jid e))) (on :ws-error conn (lambda (e) (wa-handle-ws-error comp conn jid e)))
(on :disconnect conn (lambda (k) (wa-handle-disconnect comp conn jid k))) (on :disconnect conn (lambda (k) (wa-handle-disconnect comp conn jid k)))
(on :chat-modified conn (lambda (k) (wa-handle-chat-modified comp conn jid k)))
(on :error conn (lambda (e backtrace) (wa-handle-error comp conn jid e backtrace))) (on :error conn (lambda (e backtrace) (wa-handle-error comp conn jid e backtrace)))
(on :error-status-code conn (lambda (e) (wa-handle-error-status-code comp conn jid e))) (on :error-status-code conn (lambda (e) (wa-handle-error-status-code comp conn jid e)))
(on :qrcode conn (lambda (text) (wa-handle-ws-qrcode comp conn jid text))) (on :qrcode conn (lambda (text) (wa-handle-ws-qrcode comp conn jid text)))
@ -1021,6 +1037,16 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
(reply "WhatsApp connections disabled.")) (reply "WhatsApp connections disabled."))
(when conn (when conn
(whatscl::close-connection conn)))) (whatscl::close-connection conn))))
((equal body "refresh-chats")
(let ((conn (gethash stripped (component-whatsapps comp))))
(if conn
(let ((chats (get-user-groupchats uid)))
(reply (format nil "Refreshing metadata for ~A groupchats...~%When the metadata refresh is complete, you'll need to rejoin all of your groupchats (most easily accomplished by reconnecting yourself to XMPP)."
(length chats)))
(loop
for (localpart . subject) in chats
do (request-wa-chat-metadata comp conn stripped localpart)))
(reply "You're not connected to WhatsApp."))))
(t (t
(reply "Unknown command. Try `help` for a list of supported commands.")))))) (reply "Unknown command. Try `help` for a list of supported commands."))))))
@ -1272,7 +1298,8 @@ Returns three values: avatar data (as two values), and a generalized boolean spe
(unless wa-jid (unless wa-jid
(return-from whatsxmpp-chat-state-handler)) (return-from whatsxmpp-chat-state-handler))
(unless conn (unless conn
(warn "Can't send chat state, since user connection is offline")) (warn "Can't send chat state, since user connection is offline")
(return-from whatsxmpp-chat-state-handler))
(whatscl::send-presence conn presence-type (whatscl::send-presence conn presence-type
(unless (eql presence-type :active) (unless (eql presence-type :active)
wa-jid)))))) wa-jid))))))