changeset 24:6c620e9f7d2c

Add support for retrieving all MAM messages, and for downloading it in a XEP-0227-like format.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sun, 23 Dec 2018 15:39:51 +0100
parents e99984564b17
children d9da5c3e305d
files index.xhtml mam.js strophe.mam.js strophe.rsm.js util.js
diffstat 5 files changed, 185 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/index.xhtml
+++ b/index.xhtml
@@ -156,8 +156,9 @@ Enter your nickname, so people you know 
 <td><textarea id="mam-prefs-never"/></td>
 </tr>
 </table>
+<button id="mam-retrieve">Retrieve my message archive</button> <img width="24" height="24" id="mam-retrieve-spinner" hidden=""/><br/>
 <button disabled="">View my message archive</button><br/>
-<button disabled="">Download my entire message archive</button><br/>
+<button id="mam-download" disabled="">Download my entire message archive</button><br/>
 <button disabled="">⚠️ Purge my entire message archive</button>
 </p>
 </div>
@@ -183,6 +184,8 @@ Enter your nickname, so people you know 
 
 <script src="util.js"/>
 <script src="strophe.js"/>
+<script src="strophe.rsm.js"/>
+<script src="strophe.mam.js"/>
 <script src="client.js"/>
 <script src="nickname.js"/>
 <script src="avatar.js"/>
--- a/mam.js
+++ b/mam.js
@@ -6,6 +6,14 @@ function initMAM(connection) {
     const prefs_never = document.getElementById('mam-prefs-never');
     const prefs_spinner = document.getElementById('mam-prefs-spinner');
 
+    const mam_retrieve = document.getElementById('mam-retrieve');
+    const download_button = document.getElementById('mam-download');
+    const retrieve_spinner = document.getElementById('mam-retrieve-spinner');
+
+    const mam_data = [];
+
+    download_button.disabled = true;
+
     const iq = $iq({type: 'get'})
         .c('prefs', {xmlns: NS.mam});
     connection.sendIQ(iq, onMAMPrefs, onMAMPrefsError.bind(null, 'query failed.'));
@@ -80,4 +88,42 @@ function initMAM(connection) {
         console.log('Error while changing MAM preferences.');
         spinnerError(prefs_spinner);
     }
+
+    mam_retrieve.addEventListener('click', function (evt) {
+        connection.mam.query(null, {
+            onMessage: function (message, delay, id) {
+                console.log('Got a MAM message:', message, delay, id);
+                const result = document.createElementNS(NS.mam, 'result');
+                result.setAttributeNS(null, 'id', id);
+                const forwarded = document.createElementNS(NS.forward, 'forwarded');
+                forwarded.appendChild(delay);
+                forwarded.appendChild(message);
+                result.appendChild(forwarded);
+                mam_data.push(result);
+            },
+            onComplete: function () {
+                console.log('Received all messages, you can now download a backup of your archive.');
+                download_button.disabled = false;
+                spinnerOk(retrieve_spinner);
+            },
+        });
+        displaySpinner(retrieve_spinner);
+    });
+
+    download_button.addEventListener('click', function (evt) {
+        // TODO: extend XEP-0227 with MAM support.
+        const messages = document.createElementNS('https://prosody.im/protocol/pie-mam', 'message-archive');
+        for (let result of mam_data)
+            messages.appendChild(result);
+        const blob = new Blob([messages.outerHTML], {type: 'text/xml'});
+        const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+        link.hidden = false;
+        link.href = URL.createObjectURL(blob);
+        link.download = Strophe.getBareJidFromJid(connection.jid) + '-archive.xml';
+        console.log(link.download);
+        document.body.appendChild(link);
+        link.click();
+        URL.revokeObjectURL(link.href);
+        document.body.removeChild(link);
+    });
 }
new file mode 100644
--- /dev/null
+++ b/strophe.mam.js
@@ -0,0 +1,82 @@
+/* XEP-0313: Message Archive Management
+ * Copyright (C) 2012 Kim Alvefur
+ * Copyright (C) 2018 Emmanuel Gil Peyrot
+ *
+ * This file is MIT/X11 licensed. Please see the
+ * LICENSE.txt file in the source package for more information.
+ *
+ * Modified by: Chris Tunbridge (github.com/Destreyf/)
+ * Updated to support v0.3 of the XMPP XEP-0313 standard
+ * http://xmpp.org/extensions/xep-0313.html
+ *
+ */
+//import { $iq, Strophe } from 'strophe.js';
+
+Strophe.addConnectionPlugin('mam', {
+    _c: null,
+    _p: [ 'with', 'start', 'end' ],
+    init: function (conn) {
+        this._c = conn;
+        Strophe.addNamespace('MAM', 'urn:xmpp:mam:2');
+        Strophe.addNamespace('Forward', 'urn:xmpp:forward:0');
+    },
+    query: function (jid, options) {
+        var _c = this._c;
+        var _p = this._p;
+        var attr = {
+            type:'set',
+            to:jid
+        };
+        options = options || {};
+        var queryid = options.queryid;
+        if (queryid) {
+            delete options.queryid;
+        } else {
+            queryid = _c.getUniqueId();
+        }
+        var iq = $iq(attr).c('query', {xmlns: Strophe.NS.MAM, queryid: queryid}).c('x',{xmlns:'jabber:x:data', type:'submit'});
+
+        iq.c('field',{var:'FORM_TYPE', type:'hidden'}).c('value').t(Strophe.NS.MAM).up().up();
+        for (var i = 0; i < _p.length; i++) {
+            var pn = _p[i];
+            var p = options[pn];
+            delete options[pn];
+            if (p) {
+                iq.c('field',{var:pn}).c('value').t(p).up().up();
+            }
+        }
+        iq.up();
+
+        var onMessage = options.onMessage;
+        delete options.onMessage;
+        var onComplete = options.onComplete;
+        delete options.onComplete;
+        iq.cnode(new Strophe.RSM(options).toXML());
+
+        var handler = _c.addHandler(function (message) {
+            // TODO: check the emitter too!
+            var result = message.firstChild;
+            if (!result || result.namespaceURI !== Strophe.NS.MAM || result.localName !== 'result' || result.getAttributeNS(null, 'queryid') !== queryid)
+                return true;
+            var id = result.getAttributeNS(null, 'id');
+            var forwarded = result.firstChild;
+            if (!forwarded || forwarded.namespaceURI !== Strophe.NS.Forward || forwarded.localName !== 'forwarded')
+                return true;
+            var delay = null;
+            var childMessage = null;
+            for (var child of forwarded.childNodes.values()) {
+                if (child.namespaceURI === 'urn:xmpp:delay' && child.localName === 'delay' && delay === null)
+                    delay = child;
+                else if (child.namespaceURI === 'jabber:client' && child.localName === 'message' && childMessage === null)
+                    childMessage = child;
+            }
+            if (childMessage !== null && delay !== null)
+                onMessage(childMessage, delay, id);
+            return true;
+        }, Strophe.NS.MAM, 'message', null);
+        return _c.sendIQ(iq, function(){
+           _c.deleteHandler(handler);
+           onComplete.apply(this, arguments);
+        });
+    }
+});
new file mode 100644
--- /dev/null
+++ b/strophe.rsm.js
@@ -0,0 +1,52 @@
+//import { $build, Strophe } from 'strophe.js';
+
+Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
+
+Strophe.RSM = function(options) {
+  this.attribs = ['max', 'first', 'last', 'after', 'before', 'index', 'count'];
+
+  if (typeof options.xml != 'undefined') {
+    this.fromXMLElement(options.xml);
+  } else {
+    for (var ii = 0; ii < this.attribs.length; ii++) {
+      var attrib = this.attribs[ii];
+      this[attrib] = options[attrib];
+    }
+  }
+};
+
+Strophe.RSM.prototype = {
+  toXML: function() {
+    var xml = $build('set', {xmlns: Strophe.NS.RSM});
+    for (var ii = 0; ii < this.attribs.length; ii++) {
+      var attrib = this.attribs[ii];
+      if (typeof this[attrib] != 'undefined') {
+        xml = xml.c(attrib).t(this[attrib].toString()).up();
+      }
+    }
+    return xml.tree();
+  },
+
+  next: function(max) {
+    var newSet = new Strophe.RSM({max: max, after: this.last});
+    return newSet;
+  },
+
+  previous: function(max) {
+    var newSet = new Strophe.RSM({max: max, before: this.first});
+    return newSet;
+  },
+
+  fromXMLElement: function(xmlElement) {
+    for (var ii = 0; ii < this.attribs.length; ii++) {
+      var attrib = this.attribs[ii];
+      var elem = xmlElement.getElementsByTagName(attrib)[0];
+      if (typeof elem != 'undefined' && elem !== null) {
+        this[attrib] = Strophe.getText(elem);
+        if (attrib == 'first') {
+          this.index = elem.getAttribute('index');
+        }
+      }
+    }
+  }
+};
--- a/util.js
+++ b/util.js
@@ -10,6 +10,7 @@ const NS = {
     avatar_data: 'urn:xmpp:avatar:data',
     nickname: 'http://jabber.org/protocol/nick',
     mam: 'urn:xmpp:mam:2',
+    forward: 'urn:xmpp:forward:0',
 };
 
 function nsResolver(prefix) {