2020-06-25 15:55:37 +00:00
( in-package :whatsxmpp )
( defun get-user-id ( jid )
"Get the user ID of JID, or NIL if none exists."
( with-prepared-statement
( get-user "SELECT id FROM users WHERE jid = ?" )
( let ( ( stripped ( strip-resource jid ) ) )
( bind-parameters get-user stripped )
( when ( sqlite:step-statement get-user )
( first ( column-values get-user ) ) ) ) ) )
2020-09-26 13:59:12 +00:00
( defun get-user-jid ( id )
"Get the user JID for the ID, or NIL if none exists."
( with-prepared-statement
( get-user "SELECT jid FROM users WHERE id = ?" )
( bind-parameters get-user id )
( when ( sqlite:step-statement get-user )
( first ( column-values get-user ) ) ) ) )
2020-06-25 15:55:37 +00:00
( defun get-user-contact-localparts ( uid )
"Returns a list of all contact localparts for UID."
( with-prepared-statements
( ( get-stmt "SELECT wa_jid FROM user_contacts WHERE user_id = ?" ) )
( bind-parameters get-stmt uid )
( loop
while ( sqlite:step-statement get-stmt )
collect ( sqlite:statement-column-value get-stmt 0 ) ) ) )
( defun get-user-chat-id ( uid localpart )
"Get the user chat ID of LOCALPART for UID, or NIL if none exists."
( with-prepared-statements
( ( get-stmt "SELECT id FROM user_chats WHERE user_id = ? AND wa_jid = ?" ) )
( bind-parameters get-stmt uid localpart )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( id ) get-stmt
id ) ) ) )
2020-09-27 22:01:40 +00:00
( defun get-user-chat-localpart ( chat-id )
"Get the user chat localpart for CHAT-ID, or NIL if none exists."
( with-prepared-statements
( ( get-stmt "SELECT wa_jid FROM user_chats WHERE id = ?" ) )
( bind-parameters get-stmt chat-id )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( localpart ) get-stmt
localpart ) ) ) )
2020-06-25 15:55:37 +00:00
( defun get-user-chat-subject ( uid localpart )
"Get the user chat subject of LOCALPART for UID, or NIL if none exists."
( with-prepared-statements
( ( get-stmt "SELECT subject FROM user_chats WHERE user_id = ? AND wa_jid = ?" ) )
( bind-parameters get-stmt uid localpart )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( subject ) get-stmt
subject ) ) ) )
( defun get-user-chat-resource ( uid localpart )
"Get the user chat resource of LOCALPART for UID, or NIL if none exists."
( with-prepared-statements
( ( get-stmt "SELECT user_resource FROM user_chats WHERE user_id = ? AND wa_jid = ?" ) )
( bind-parameters get-stmt uid localpart )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( resource ) get-stmt
( when ( and resource ( > ( length resource ) 0 ) )
resource ) ) ) ) )
( defun get-participant-resource ( chat-id localpart )
"Get the participant resource for LOCALPART in CHAT-ID, or NIL if none exists."
( with-prepared-statements
( ( get-stmt "SELECT resource FROM user_chat_members WHERE chat_id = ? AND wa_jid = ?" ) )
( bind-parameters get-stmt chat-id localpart )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( resource ) get-stmt
( when ( and resource ( > ( length resource ) 0 ) )
resource ) ) ) ) )
( defun get-user-chat-joined ( uid localpart )
"Get the user chat resource of LOCALPART for UID, or NIL if none exists."
( with-prepared-statements
( ( get-stmt "SELECT ucj.jid FROM user_chats AS uc, user_chat_joined AS ucj WHERE uc.user_id = ? AND uc.wa_jid = ? AND uc.id = ucj.chat_id" ) )
( bind-parameters get-stmt uid localpart )
( loop
while ( sqlite:step-statement get-stmt )
append ( column-values get-stmt ) ) ) )
2020-08-21 18:40:25 +00:00
( defun get-contact-name ( uid localpart &key no-phone-number )
2020-06-25 15:55:37 +00:00
"Get a name for LOCALPART, a possible contact for the user with ID UID."
( with-prepared-statements
( ( get-stmt "SELECT name, notify FROM user_contacts WHERE user_id = ? AND wa_jid = ?" ) )
( bind-parameters get-stmt uid localpart )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( name notify ) get-stmt
2020-08-21 18:40:25 +00:00
( or name notify ( unless no-phone-number ( substitute #\+ #\u localpart ) ) ) ) ) ) )
2020-06-25 15:55:37 +00:00
( defun get-contact-status ( uid localpart )
"Get the contact status text for LOCALPART, a possible contact for the user with ID UID."
( declare ( type integer uid ) ( type string localpart ) )
( with-prepared-statements
( ( get-stmt "SELECT status FROM user_contacts WHERE user_id = ? AND wa_jid = ?" ) )
( bind-parameters get-stmt uid localpart )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( status ) get-stmt
status ) ) ) )
( defun insert-user-message ( uid xmpp-id wa-id )
"Inserts a mapping between the message IDs XMPP-ID and WA-ID for the user UID."
( with-prepared-statements
( ( insert-stmt "INSERT INTO user_messages (user_id, xmpp_id, wa_id) VALUES (?, ?, ?)" ) )
( bind-parameters insert-stmt uid xmpp-id wa-id )
( sqlite:step-statement insert-stmt ) ) )
( defun insert-user-chat ( uid wa-id )
"Inserts a user chat with localpart WA-ID into the database for the user with UID."
( with-prepared-statements
2020-08-02 11:59:59 +00:00
( ( insert-stmt "INSERT INTO user_chats (user_id, wa_jid) VALUES (?, ?) ON CONFLICT DO NOTHING" ) )
2020-06-25 15:55:37 +00:00
( bind-parameters insert-stmt uid wa-id )
( sqlite:step-statement insert-stmt ) ) )
( defun lookup-wa-msgid ( uid wa-msgid )
"Look up the XMPP ID for the WhatsApp message ID WA-MSGID, when received for the user UID."
( with-prepared-statements
( ( get-stmt "SELECT xmpp_id FROM user_messages WHERE user_id = ? AND wa_id = ?" ) )
( bind-parameters get-stmt uid wa-msgid )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( xid ) get-stmt
xid ) ) ) )
( defun lookup-xmpp-msgid ( uid xmpp-msgid )
"Look up the WhatsApp message ID for the XMPP message ID XMPP-MSGID, when received for the user UID."
( with-prepared-statements
( ( get-stmt "SELECT wa_id FROM user_messages WHERE user_id = ? AND xmpp_id = ?" ) )
( bind-parameters get-stmt uid xmpp-msgid )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( wid ) get-stmt
wid ) ) ) )
( defun get-contact-localparts ( uid )
"Get a list of contact localparts for the user with ID UID."
( with-prepared-statements
( ( get-stmt "SELECT wa_jid FROM user_contacts WHERE user_id = ?" ) )
( bind-parameters get-stmt uid )
( loop
while ( sqlite:step-statement get-stmt )
collect ( with-bound-columns ( localpart ) get-stmt localpart ) ) ) )
2020-07-07 20:27:21 +00:00
( defun get-user-groupchats ( uid )
"Get a list of groupchat info (cons pairs of LOCALPART . SUBJECT) for the user with ID UID."
( with-prepared-statements
( ( get-stmt "SELECT wa_jid, subject FROM user_chats WHERE user_id = ?" ) )
( bind-parameters get-stmt uid )
( loop
while ( sqlite:step-statement get-stmt )
collect ( with-bound-columns ( localpart subject ) get-stmt ( cons localpart subject ) ) ) ) )
Save groupchat messages in an archive, allow bulk history requesting
- WARNING: This change is hilariously unsuitable for public instances. Don't run
this code in such an environment yet!
- Groupchat messages are now stored in the sqlite3 database, with the intent
to allow retrieval via MAM at a later date.
FIXME: You can't opt out of this though, which is a huge GDPR hole.
- You can also request ALL of your group chat history from whatsapp be fetched
and stored in the database (!!). This is a VERY resource-intensive operation,
as it involves spawning a metric crapton of threads, uploading a metric
crapton of historical media to the configured upload server, and writing a
metric crapton of messages into the database.
- At some point, the ability to invoke this will be severely limited to only
approved users. That point has not yet come, though.
- Additionally, the chat history request will abort if the connection it's
associated with dies. (You can just retry it, though.)
2020-09-26 18:49:26 +00:00
2021-05-28 15:53:42 +00:00
( defun get-contact-chat-subjects ( uid wa-jid )
"Get a list of chat subjects for each chat the remote contact identifed by WA-JID is in, for the user with ID UID."
( with-prepared-statements
( ( get-stmt "SELECT DISTINCT user_chats.subject FROM user_chats, user_chat_members WHERE user_chats.id = user_chat_members.chat_id AND user_chat_members.wa_jid = ? AND user_chats.user_id = ?" ) )
( bind-parameters get-stmt wa-jid uid )
( loop
while ( sqlite:step-statement get-stmt )
collect ( with-bound-columns ( subject ) get-stmt subject ) ) ) )
Save groupchat messages in an archive, allow bulk history requesting
- WARNING: This change is hilariously unsuitable for public instances. Don't run
this code in such an environment yet!
- Groupchat messages are now stored in the sqlite3 database, with the intent
to allow retrieval via MAM at a later date.
FIXME: You can't opt out of this though, which is a huge GDPR hole.
- You can also request ALL of your group chat history from whatsapp be fetched
and stored in the database (!!). This is a VERY resource-intensive operation,
as it involves spawning a metric crapton of threads, uploading a metric
crapton of historical media to the configured upload server, and writing a
metric crapton of messages into the database.
- At some point, the ability to invoke this will be severely limited to only
approved users. That point has not yet come, though.
- Additionally, the chat history request will abort if the connection it's
associated with dies. (You can just retry it, though.)
2020-09-26 18:49:26 +00:00
( defun insert-xmpp-message ( xm )
"Inserts XM, a groupchat XMPP-MESSAGE, into the database."
( assert ( uiop:string-prefix-p "g" ( conversation xm ) ) ( ) "Tried to insert XMPP message for non-groupchat conversation ~A" ( conversation xm ) )
2020-09-27 22:01:40 +00:00
( let* ( ( chat-id ( or
( get-user-chat-id ( uid xm ) ( conversation xm ) )
( error "Couldn't find chat id for conversation ~A / uid ~A"
( conversation xm ) ( uid xm ) ) ) )
( local-time:*default-timezone* local-time:+utc-zone+ )
( ts-unix ( local-time:timestamp-to-unix ( timestamp xm ) ) ) )
Save groupchat messages in an archive, allow bulk history requesting
- WARNING: This change is hilariously unsuitable for public instances. Don't run
this code in such an environment yet!
- Groupchat messages are now stored in the sqlite3 database, with the intent
to allow retrieval via MAM at a later date.
FIXME: You can't opt out of this though, which is a huge GDPR hole.
- You can also request ALL of your group chat history from whatsapp be fetched
and stored in the database (!!). This is a VERY resource-intensive operation,
as it involves spawning a metric crapton of threads, uploading a metric
crapton of historical media to the configured upload server, and writing a
metric crapton of messages into the database.
- At some point, the ability to invoke this will be severely limited to only
approved users. That point has not yet come, though.
- Additionally, the chat history request will abort if the connection it's
associated with dies. (You can just retry it, though.)
2020-09-26 18:49:26 +00:00
( with-prepared-statements
( ( insert-stmt "INSERT INTO user_chat_history (user_id, chat_id, user_from, ts_unix, xmpp_id, orig_id, body, oob_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" ) )
( bind-parameters insert-stmt ( 1 ( uid xm ) ) ( 2 chat-id ) ( 3 ( from xm ) ) ( 4 ts-unix ) ( 5 ( xmpp-id xm ) ) ( 6 ( orig-id xm ) ) ( 7 ( body xm ) ) ( 8 ( oob-url xm ) ) )
( sqlite:step-statement insert-stmt ) ) ) )
( defun lookup-wa-msgid-in-history ( uid wa-msgid )
"Look up the XMPP ID for the WhatsApp message ID WA-MSGID, when received for the user UID."
( with-prepared-statements
( ( get-stmt "SELECT xmpp_id FROM user_chat_history WHERE user_id = ? AND orig_id = ?" ) )
( bind-parameters get-stmt uid wa-msgid )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( xid ) get-stmt
xid ) ) ) )
2020-09-27 22:01:40 +00:00
2020-09-30 13:01:52 +00:00
( defun user-archiving-enabled-p ( uid )
"Returns a generalized boolean for whether the user with ID UID has archiving enabled or not."
( with-prepared-statements
( ( get-stmt "SELECT enable_archiving FROM users WHERE id = ?" ) )
( bind-parameters get-stmt uid )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( ena ) get-stmt
( not ( eql ena 0 ) ) ) ) ) )
( defun user-set-archiving-state ( uid enabled )
"Set the user's archiving state for the user with ID UID to ENABLED (either T or NIL)."
( let ( ( ena ( if enabled 1 0 ) ) )
( with-prepared-statements
( ( set-stmt "UPDATE users SET enable_archiving = ? WHERE id = ?" ) )
( bind-parameters set-stmt ena uid )
( sqlite:step-statement set-stmt ) ) ) )
( defun jid-admin-p ( jid )
"Returns a generalized boolean for whether the JID is a bridge administrator."
( with-prepared-statements
( ( get-stmt "SELECT id FROM administrators WHERE jid = ?" ) )
( bind-parameters get-stmt jid )
( when ( sqlite:step-statement get-stmt )
t ) ) )
( defun db-unregister-user ( uid )
"Unregister the user with ID UID."
( with-prepared-statements
( ( remove-user-stmt "DELETE FROM users WHERE id = ?" )
( remove-contacts-stmt "DELETE FROM user_contacts WHERE user_id = ?" )
( remove-messages-stmt "DELETE FROM user_messages WHERE user_id = ?" )
( remove-chats-stmt "DELETE FROM user_chats WHERE user_id = ?" )
( get-chats-stmt "SELECT id FROM user_chats WHERE user_id = ?" )
( remove-chat-members-stmt "DELETE FROM user_chat_members WHERE chat_id = ?" )
( remove-chat-joined-stmt "DELETE FROM user_chat_joined WHERE chat_id = ?" )
( remove-chat-history-stmt "DELETE FROM user_chat_history WHERE user_id = ?" ) )
( with-transaction ( )
( bind-parameters get-chats-stmt uid )
( loop
while ( sqlite:step-statement get-chats-stmt )
do ( with-bound-columns ( chatid ) get-chats-stmt
( loop
for stmt in ( list remove-chat-members-stmt remove-chat-joined-stmt remove-chat-history-stmt )
do ( progn
( sqlite:reset-statement stmt )
( bind-parameters stmt chatid )
( sqlite:step-statement stmt ) ) ) ) )
( loop
for stmt in ( list remove-chats-stmt remove-messages-stmt remove-contacts-stmt remove-user-stmt )
do ( progn
( sqlite:reset-statement stmt )
( bind-parameters stmt uid )
( sqlite:step-statement stmt ) ) ) ) ) )
2020-09-27 22:01:40 +00:00
( defun get-chat-history-ts ( uid chat-id xmpp-id )
"Look up the UNIX timestamp for the given UID, CHAT-ID and XMPP-ID."
( with-prepared-statements
( ( get-stmt "SELECT ts_unix FROM user_chat_history WHERE user_id = ? AND chat_id = ? AND xmpp_id = ?" ) )
( bind-parameters get-stmt uid chat-id xmpp-id )
( when ( sqlite:step-statement get-stmt )
( with-bound-columns ( tsu ) get-stmt
tsu ) ) ) )
( defun query-archive ( uid chat-id &key start end ( limit 100 ) reference-stanza-id forward-page )
" Query the chat history archive for the chat identified by CHAT-ID and UID. Optionally narrow the query using START and END ( UNIX timestamps ) , returning at most LIMIT items ( which is clamped to 100 ) .
If an RSM REFERENCE-STANZA-ID is provided, narrow the query to be either after ( T ) or before ( NIL ) the history entry with that stanza ID, depending on the value of FORWARD-PAGE ( see brackets ) . "
( let ( ( statement ( make-string-output-stream ) )
( localpart ( get-user-chat-localpart chat-id ) )
( local-time:*default-timezone* local-time:+utc-zone+ )
( args ( list chat-id uid ) ) ; WARNING this list is nreverse'd later!
( items-returned 0 )
( sqlite-stmt ) )
( format statement "SELECT user_from, ts_unix, xmpp_id, orig_id, body, oob_url FROM user_chat_history WHERE user_id = ? AND chat_id = ?" )
( when reference-stanza-id
( let ( ( reference-ts ( or
( get-chat-history-ts uid chat-id reference-stanza-id )
( error "Couldn't locate reference stanza ID ~A" reference-stanza-id ) ) ) )
( if forward-page
( setf start reference-ts )
( setf end reference-ts ) ) ) )
( when start
( format statement " AND ts_unix > ?" )
( push start args ) )
( when end
( format statement " AND ts_unix < ?" )
( push end args ) )
( unless limit
( setf limit 100 ) )
( when ( > limit 100 )
( setf limit 100 ) ) ; clamp me owo
;; We copy a trick from biboumi: in order to figure out whether there are
;; more results if not for the limit existing, simply increment the limit
;; by 1 and see if you get the extra element.
( format statement " ORDER BY ts_unix ~A LIMIT ~A" ( if forward-page "ASC" "DESC" ) ( 1+ limit ) )
( setf args ( nreverse args ) )
( bt:with-recursive-lock-held ( *db-lock* )
( let ( ( stmt-text ( get-output-stream-string statement ) ) )
( setf sqlite-stmt ( sqlite:prepare-statement *db* stmt-text ) ) )
( loop
for param in args
for n from 1
do ( sqlite:bind-parameter sqlite-stmt n param ) )
( values
( funcall
( if forward-page #' identity #' nreverse )
( loop
while ( sqlite:step-statement sqlite-stmt )
do ( incf items-returned )
while ( <= items-returned limit )
collect ( with-bound-columns ( from ts-unix xmpp-id orig-id body oob-url ) sqlite-stmt
( make-instance 'xmpp-message
:uid uid
:conversation localpart
:from from
:timestamp ( local-time:unix-to-timestamp ts-unix )
:xmpp-id xmpp-id
:orig-id orig-id
:body body
:oob-url oob-url ) ) ) )
( <= items-returned limit ) ) ) ) )