IqGenerator.java

  1package eu.siacs.conversations.generator;
  2
  3import android.os.Bundle;
  4import android.util.Base64;
  5import android.util.Base64OutputStream;
  6import android.util.Log;
  7
  8import com.cheogram.android.BobTransfer;
  9
 10import com.google.common.base.Strings;
 11import com.google.common.io.ByteStreams;
 12
 13import org.whispersystems.libsignal.IdentityKey;
 14import org.whispersystems.libsignal.ecc.ECPublicKey;
 15import org.whispersystems.libsignal.state.PreKeyRecord;
 16import org.whispersystems.libsignal.state.SignedPreKeyRecord;
 17
 18import java.io.ByteArrayOutputStream;
 19import java.io.FileInputStream;
 20import java.io.IOException;
 21import eu.siacs.conversations.Config;
 22import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 23import eu.siacs.conversations.entities.Account;
 24import eu.siacs.conversations.entities.Bookmark;
 25import eu.siacs.conversations.entities.Conversation;
 26import eu.siacs.conversations.entities.DownloadableFile;
 27import eu.siacs.conversations.services.MessageArchiveService;
 28import eu.siacs.conversations.services.XmppConnectionService;
 29import eu.siacs.conversations.xml.Element;
 30import eu.siacs.conversations.xml.Namespace;
 31import eu.siacs.conversations.xmpp.Jid;
 32import eu.siacs.conversations.xmpp.forms.Data;
 33import eu.siacs.conversations.xmpp.pep.Avatar;
 34import im.conversations.android.xmpp.model.stanza.Iq;
 35import im.conversations.android.xmpp.model.upload.Request;
 36import java.nio.ByteBuffer;
 37import java.security.cert.CertificateEncodingException;
 38import java.security.cert.X509Certificate;
 39import java.util.ArrayList;
 40import java.util.List;
 41import java.util.Locale;
 42import java.util.Set;
 43import java.util.TimeZone;
 44import java.util.UUID;
 45
 46import io.ipfs.cid.Cid;
 47
 48import eu.siacs.conversations.Config;
 49import eu.siacs.conversations.R;
 50import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 51import eu.siacs.conversations.entities.Account;
 52import eu.siacs.conversations.entities.Bookmark;
 53import eu.siacs.conversations.entities.Conversation;
 54import eu.siacs.conversations.entities.DownloadableFile;
 55import eu.siacs.conversations.entities.Message;
 56import eu.siacs.conversations.services.MessageArchiveService;
 57import eu.siacs.conversations.services.QuickConversationsService;
 58import eu.siacs.conversations.services.XmppConnectionService;
 59import eu.siacs.conversations.xml.Element;
 60import eu.siacs.conversations.xml.Namespace;
 61import eu.siacs.conversations.xmpp.Jid;
 62import eu.siacs.conversations.xmpp.forms.Data;
 63import eu.siacs.conversations.xmpp.pep.Avatar;
 64import im.conversations.android.xmpp.model.stanza.Iq;
 65
 66public class IqGenerator extends AbstractGenerator {
 67
 68    public IqGenerator(final XmppConnectionService service) {
 69        super(service);
 70    }
 71
 72    public Iq entityTimeResponse(final Iq request) {
 73        final Iq packet = request.generateResponse(Iq.Type.RESULT);
 74        Element time = packet.addChild("time", "urn:xmpp:time");
 75        final long now = System.currentTimeMillis();
 76        time.addChild("utc").setContent(getTimestamp(now));
 77        TimeZone ourTimezone = TimeZone.getDefault();
 78        long offsetSeconds = ourTimezone.getOffset(now) / 1000;
 79        long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60);
 80        long offsetHours = offsetSeconds / 3600;
 81        String hours;
 82        if (offsetHours < 0) {
 83            hours = String.format(Locale.US, "%03d", offsetHours);
 84        } else {
 85            hours = String.format(Locale.US, "%02d", offsetHours);
 86        }
 87        String minutes = String.format(Locale.US, "%02d", offsetMinutes);
 88        time.addChild("tzo").setContent(hours + ":" + minutes);
 89        return packet;
 90    }
 91
 92    public static Iq purgeOfflineMessages() {
 93        final Iq packet = new Iq(Iq.Type.SET);
 94        packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
 95        return packet;
 96    }
 97
 98    protected Iq publish(final String node, final Element item, final Bundle options) {
 99        final var packet = new Iq(Iq.Type.SET);
100        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
101        final Element publish = pubsub.addChild("publish");
102        publish.setAttribute("node", node);
103        publish.addChild(item);
104        if (options != null) {
105            final Element publishOptions = pubsub.addChild("publish-options");
106            publishOptions.addChild(Data.create(Namespace.PUBSUB_PUBLISH_OPTIONS, options));
107        }
108        return packet;
109    }
110
111    protected Iq publish(final String node, final Element item) {
112        return publish(node, item, null);
113    }
114
115    private Iq retrieve(String node, Element item) {
116        final var packet = new Iq(Iq.Type.GET);
117        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
118        final Element items = pubsub.addChild("items");
119        items.setAttribute("node", node);
120        if (item != null) {
121            items.addChild(item);
122        }
123        return packet;
124    }
125
126    public Iq retrieveVcard4(final Jid jid) {
127        final var packet = retrieve("urn:xmpp:vcard4", null);
128        packet.setTo(jid);
129        return packet;
130    }
131
132    public Iq retrieveBookmarks() {
133        return retrieve(Namespace.BOOKMARKS2, null);
134    }
135
136    public Iq retrieveMds() {
137        return retrieve(Namespace.MDS_DISPLAYED, null);
138    }
139
140    public Iq publishNick(String nick) {
141        final Element item = new Element("item");
142        item.setAttribute("id", "current");
143        item.addChild("nick", Namespace.NICK).setContent(nick);
144        return publish(Namespace.NICK, item);
145    }
146
147    public Iq deleteNode(final String node) {
148        final var packet = new Iq(Iq.Type.SET);
149        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
150        pubsub.addChild("delete").setAttribute("node", node);
151        return packet;
152    }
153
154    public Iq deleteItem(final String node, final String id) {
155        final var packet = new Iq(Iq.Type.SET);
156        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
157        final Element retract = pubsub.addChild("retract");
158        retract.setAttribute("node", node);
159        retract.setAttribute("notify", "true");
160        retract.addChild("item").setAttribute("id", id);
161        return packet;
162    }
163
164    public Iq publishAvatar(Avatar avatar, Bundle options) {
165        final Element item = new Element("item");
166        item.setAttribute("id", avatar.sha1sum);
167        final Element data = item.addChild("data", Namespace.AVATAR_DATA);
168        data.setContent(avatar.image);
169        return publish(Namespace.AVATAR_DATA, item, options);
170    }
171
172    public Iq publishElement(
173            final String namespace, final Element element, String id, final Bundle options) {
174        final Element item = new Element("item");
175        item.setAttribute("id", id);
176        item.addChild(element);
177        return publish(namespace, item, options);
178    }
179
180    public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) {
181        final Element item = new Element("item");
182        item.setAttribute("id", avatar.sha1sum);
183        final Element metadata = item.addChild("metadata", Namespace.AVATAR_METADATA);
184        final Element info = metadata.addChild("info");
185        info.setAttribute("bytes", avatar.size);
186        info.setAttribute("id", avatar.sha1sum);
187        info.setAttribute("height", avatar.height);
188        info.setAttribute("width", avatar.height);
189        info.setAttribute("type", avatar.type);
190        return publish(Namespace.AVATAR_METADATA, item, options);
191    }
192
193    public Iq retrievePepAvatar(final Avatar avatar) {
194        final Element item = new Element("item");
195        item.setAttribute("id", avatar.sha1sum);
196        final var packet = retrieve(Namespace.AVATAR_DATA, item);
197        packet.setTo(avatar.owner);
198        return packet;
199    }
200
201    public Iq retrieveVcardAvatar(final Avatar avatar) {
202        final Iq packet = new Iq(Iq.Type.GET);
203        packet.setTo(avatar.owner);
204        packet.addChild("vCard", "vcard-temp");
205        return packet;
206    }
207
208    public Iq retrieveVcardAvatar(final Jid to) {
209        final Iq packet = new Iq(Iq.Type.GET);
210        packet.setTo(to);
211        packet.addChild("vCard", "vcard-temp");
212        return packet;
213    }
214
215    public Iq retrieveAvatarMetaData(final Jid to) {
216        final Iq packet = retrieve("urn:xmpp:avatar:metadata", null);
217        if (to != null) {
218            packet.setTo(to);
219        }
220        return packet;
221    }
222
223    public Iq retrieveDeviceIds(final Jid to) {
224        final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
225        if (to != null) {
226            packet.setTo(to);
227        }
228        return packet;
229    }
230
231    public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) {
232        final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
233        packet.setTo(to);
234        return packet;
235    }
236
237    public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) {
238        final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
239        packet.setTo(to);
240        return packet;
241    }
242
243    public Iq publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
244        final Element item = new Element("item");
245        item.setAttribute("id", "current");
246        final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
247        for (Integer id : ids) {
248            final Element device = new Element("device");
249            device.setAttribute("id", id);
250            list.addChild(device);
251        }
252        return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions);
253    }
254
255    public Element publishBookmarkItem(final Bookmark bookmark) {
256        final String name = bookmark.getBookmarkName();
257        final String nick = bookmark.getNick();
258        final String password = bookmark.getPassword();
259        final boolean autojoin = bookmark.autojoin();
260        final Element conference = new Element("conference", Namespace.BOOKMARKS2);
261        if (!Strings.isNullOrEmpty(name)) {
262            conference.setAttribute("name", name);
263        }
264        if (!Strings.isNullOrEmpty(nick)) {
265            conference.addChild("nick").setContent(nick);
266        }
267        if (password != null) {
268            conference.addChild("password").setContent(password);
269        }
270        conference.setAttribute("autojoin", String.valueOf(autojoin));
271        conference.addChild(bookmark.getExtensions());
272        return conference;
273    }
274
275    public Element mdsDisplayed(final String stanzaId, final Conversation conversation) {
276        final Jid by;
277        if (conversation.getMode() == Conversation.MODE_MULTI) {
278            by = conversation.getJid().asBareJid();
279        } else {
280            by = conversation.getAccount().getJid().asBareJid();
281        }
282        return mdsDisplayed(stanzaId, by);
283    }
284
285    private Element mdsDisplayed(final String stanzaId, final Jid by) {
286        final Element displayed = new Element("displayed", Namespace.MDS_DISPLAYED);
287        final Element stanzaIdElement = displayed.addChild("stanza-id", Namespace.STANZA_IDS);
288        stanzaIdElement.setAttribute("id", stanzaId);
289        stanzaIdElement.setAttribute("by", by);
290        return displayed;
291    }
292
293    public Iq publishBundles(
294            final SignedPreKeyRecord signedPreKeyRecord,
295            final IdentityKey identityKey,
296            final Set<PreKeyRecord> preKeyRecords,
297            final int deviceId,
298            Bundle publishOptions) {
299        final Element item = new Element("item");
300        item.setAttribute("id", "current");
301        final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
302        final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
303        signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
304        ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
305        signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.NO_WRAP));
306        final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
307        signedPreKeySignature.setContent(
308                Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.NO_WRAP));
309        final Element identityKeyElement = bundle.addChild("identityKey");
310        identityKeyElement.setContent(
311                Base64.encodeToString(identityKey.serialize(), Base64.NO_WRAP));
312
313        final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
314        for (PreKeyRecord preKeyRecord : preKeyRecords) {
315            final Element prekey = prekeys.addChild("preKeyPublic");
316            prekey.setAttribute("preKeyId", preKeyRecord.getId());
317            prekey.setContent(
318                    Base64.encodeToString(
319                            preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.NO_WRAP));
320        }
321
322        return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions);
323    }
324
325    public Iq publishVerification(
326            byte[] signature, X509Certificate[] certificates, final int deviceId) {
327        final Element item = new Element("item");
328        item.setAttribute("id", "current");
329        final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
330        final Element chain = verification.addChild("chain");
331        for (int i = 0; i < certificates.length; ++i) {
332            try {
333                Element certificate = chain.addChild("certificate");
334                certificate.setContent(
335                        Base64.encodeToString(certificates[i].getEncoded(), Base64.NO_WRAP));
336                certificate.setAttribute("index", i);
337            } catch (CertificateEncodingException e) {
338                Log.d(Config.LOGTAG, "could not encode certificate");
339            }
340        }
341        verification
342                .addChild("signature")
343                .setContent(Base64.encodeToString(signature, Base64.NO_WRAP));
344        return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item);
345    }
346
347    public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
348        final Iq packet = new Iq(Iq.Type.SET);
349        final Element query = packet.query(mam.version.namespace);
350        query.setAttribute("queryid", mam.getQueryId());
351        final Data data = new Data();
352        data.setFormType(mam.version.namespace);
353        if (mam.muc()) {
354            packet.setTo(mam.getWith());
355        } else if (mam.getWith() != null) {
356            data.put("with", mam.getWith().toString());
357        }
358        final long start = mam.getStart();
359        final long end = mam.getEnd();
360        if (start != 0) {
361            data.put("start", getTimestamp(start));
362        }
363        if (end != 0) {
364            data.put("end", getTimestamp(end));
365        }
366        data.submit();
367        query.addChild(data);
368        Element set = query.addChild("set", "http://jabber.org/protocol/rsm");
369        if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
370            set.addChild("before").setContent(mam.getReference());
371        } else if (mam.getReference() != null) {
372            set.addChild("after").setContent(mam.getReference());
373        }
374        set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE));
375        return packet;
376    }
377
378    public Iq generateGetBlockList() {
379        final Iq iq = new Iq(Iq.Type.GET);
380        iq.addChild("blocklist", Namespace.BLOCKING);
381
382        return iq;
383    }
384
385    public Iq generateSetBlockRequest(
386            final Jid jid, final boolean reportSpam, final String serverMsgId) {
387        final Iq iq = new Iq(Iq.Type.SET);
388        final Element block = iq.addChild("block", Namespace.BLOCKING);
389        final Element item = block.addChild("item").setAttribute("jid", jid);
390        if (reportSpam) {
391            final Element report = item.addChild("report", Namespace.REPORTING);
392            report.setAttribute("reason", Namespace.REPORTING_REASON_SPAM);
393            if (serverMsgId != null) {
394                final Element stanzaId = report.addChild("stanza-id", Namespace.STANZA_IDS);
395                stanzaId.setAttribute("by", jid);
396                stanzaId.setAttribute("id", serverMsgId);
397            }
398        }
399        Log.d(Config.LOGTAG, iq.toString());
400        return iq;
401    }
402
403    public Iq generateSetUnblockRequest(final Jid jid) {
404        final Iq iq = new Iq(Iq.Type.SET);
405        final Element block = iq.addChild("unblock", Namespace.BLOCKING);
406        block.addChild("item").setAttribute("jid", jid);
407        return iq;
408    }
409
410    public Iq generateSetPassword(final Account account, final String newPassword) {
411        final Iq packet = new Iq(Iq.Type.SET);
412        packet.setTo(account.getDomain());
413        final Element query = packet.addChild("query", Namespace.REGISTER);
414        final Jid jid = account.getJid();
415        query.addChild("username").setContent(jid.getLocal());
416        query.addChild("password").setContent(newPassword);
417        return packet;
418    }
419
420    public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
421        List<Jid> jids = new ArrayList<>();
422        jids.add(jid);
423        return changeAffiliation(conference, jids, affiliation);
424    }
425
426    public Iq changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
427        final Iq packet = new Iq(Iq.Type.SET);
428        packet.setTo(conference.getJid().asBareJid());
429        packet.setFrom(conference.getAccount().getJid());
430        Element query = packet.query("http://jabber.org/protocol/muc#admin");
431        for (Jid jid : jids) {
432            Element item = query.addChild("item");
433            item.setAttribute("jid", jid);
434            item.setAttribute("affiliation", affiliation);
435        }
436        return packet;
437    }
438
439    public Iq changeRole(Conversation conference, String nick, String role) {
440        final Iq packet = new Iq(Iq.Type.SET);
441        packet.setTo(conference.getJid().asBareJid());
442        packet.setFrom(conference.getAccount().getJid());
443        Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
444        item.setAttribute("nick", nick);
445        item.setAttribute("role", role);
446        return packet;
447    }
448
449    public Iq moderateMessage(Account account, Message m, String reason) {
450        final var packet = new Iq(Iq.Type.SET);
451        packet.setTo(m.getConversation().getJid().asBareJid());
452        packet.setFrom(account.getJid());
453        final var moderate =
454            packet.addChild("apply-to", "urn:xmpp:fasten:0")
455                  .setAttribute("id", m.getServerMsgId())
456                  .addChild("moderate", "urn:xmpp:message-moderate:0");
457        moderate.addChild("retract", "urn:xmpp:message-retract:0");
458        moderate.addChild("reason", "urn:xmpp:message-moderate:0").setContent(reason);
459        return packet;
460    }
461
462    public Iq requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) {
463        final Iq packet = new Iq(Iq.Type.GET);
464        packet.setTo(host);
465        Element request = packet.addChild("request", Namespace.HTTP_UPLOAD);
466        request.setAttribute("filename", name == null ? convertFilename(file.getName()) : name);
467        request.setAttribute("size", file.getExpectedSize());
468        request.setAttribute("content-type", mime);
469        return packet;
470    }
471
472    public Iq requestHttpUploadSlot(
473            final Jid host, final DownloadableFile file, final String mime) {
474        final Iq packet = new Iq(Iq.Type.GET);
475        packet.setTo(host);
476        final var request = packet.addExtension(new Request());
477        request.setFilename(convertFilename(file.getName()));
478        request.setSize(file.getExpectedSize());
479        return packet;
480    }
481
482    private static String convertFilename(String name) {
483        int pos = name.indexOf('.');
484        if (pos != -1) {
485            try {
486                UUID uuid = UUID.fromString(name.substring(0, pos));
487                ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
488                bb.putLong(uuid.getMostSignificantBits());
489                bb.putLong(uuid.getLeastSignificantBits());
490                return Base64.encodeToString(
491                                bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
492                        + name.substring(pos);
493            } catch (Exception e) {
494                return name;
495            }
496        } else {
497            return name;
498        }
499    }
500
501    public static Iq generateCreateAccountWithCaptcha(
502            final Account account, final String id, final Data data) {
503        final Iq register = new Iq(Iq.Type.SET);
504        register.setFrom(account.getJid().asBareJid());
505        register.setTo(account.getDomain());
506        register.setId(id);
507        Element query = register.query(Namespace.REGISTER);
508        if (data != null) {
509            query.addChild(data);
510        }
511        return register;
512    }
513
514    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
515        return pushTokenToAppServer(appServer, token, deviceId, null);
516    }
517
518    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
519        final Iq packet = new Iq(Iq.Type.SET);
520        packet.setTo(appServer);
521        final Element command = packet.addChild("command", Namespace.COMMANDS);
522        command.setAttribute("node", "register-push-fcm");
523        command.setAttribute("action", "execute");
524        final Data data = new Data();
525        data.put("token", token);
526        data.put("android-id", deviceId);
527        if (muc != null) {
528            data.put("muc", muc.toString());
529        }
530        data.submit();
531        command.addChild(data);
532        return packet;
533    }
534
535    public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
536        final Iq packet = new Iq(Iq.Type.SET);
537        packet.setTo(appServer);
538        final Element command = packet.addChild("command", Namespace.COMMANDS);
539        command.setAttribute("node", "unregister-push-fcm");
540        command.setAttribute("action", "execute");
541        final Data data = new Data();
542        data.put("channel", channel);
543        data.put("android-id", deviceId);
544        data.submit();
545        command.addChild(data);
546        return packet;
547    }
548
549    public Iq enablePush(final Jid jid, final String node, final String secret) {
550        final Iq packet = new Iq(Iq.Type.SET);
551        Element enable = packet.addChild("enable", Namespace.PUSH);
552        enable.setAttribute("jid", jid);
553        enable.setAttribute("node", node);
554        if (secret != null) {
555            Data data = new Data();
556            data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
557            data.put("secret", secret);
558            data.submit();
559            enable.addChild(data);
560        }
561        return packet;
562    }
563
564    public Iq disablePush(final Jid jid, final String node) {
565        Iq packet = new Iq(Iq.Type.SET);
566        Element disable = packet.addChild("disable", Namespace.PUSH);
567        disable.setAttribute("jid", jid);
568        disable.setAttribute("node", node);
569        return packet;
570    }
571
572    public Iq queryAffiliation(Conversation conversation, String affiliation) {
573        final Iq packet = new Iq(Iq.Type.GET);
574        packet.setTo(conversation.getJid().asBareJid());
575        packet.query("http://jabber.org/protocol/muc#admin")
576                .addChild("item")
577                .setAttribute("affiliation", affiliation);
578        return packet;
579    }
580
581    public static Bundle defaultGroupChatConfiguration() {
582        Bundle options = new Bundle();
583        options.putString("muc#roomconfig_persistentroom", "1");
584        options.putString("muc#roomconfig_membersonly", "1");
585        options.putString("muc#roomconfig_publicroom", "0");
586        options.putString("muc#roomconfig_whois", "anyone");
587        options.putString("muc#roomconfig_changesubject", "0");
588        options.putString("muc#roomconfig_allowinvites", "0");
589        options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
590        options.putString("mam", "1"); // ejabberd community
591        options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
592        return options;
593    }
594
595    public static Bundle defaultChannelConfiguration() {
596        Bundle options = new Bundle();
597        options.putString("muc#roomconfig_persistentroom", "1");
598        options.putString("muc#roomconfig_membersonly", "0");
599        options.putString("muc#roomconfig_publicroom", "1");
600        options.putString("muc#roomconfig_whois", "moderators");
601        options.putString("muc#roomconfig_changesubject", "0");
602        options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
603        options.putString("mam", "1"); // ejabberd community
604        options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
605        return options;
606    }
607
608    public Iq requestPubsubConfiguration(Jid jid, String node) {
609        return pubsubConfiguration(jid, node, null);
610    }
611
612    public Iq publishPubsubConfiguration(Jid jid, String node, Data data) {
613        return pubsubConfiguration(jid, node, data);
614    }
615
616    private Iq pubsubConfiguration(Jid jid, String node, Data data) {
617        final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET);
618        packet.setTo(jid);
619        Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
620        Element configure = pubsub.addChild("configure").setAttribute("node", node);
621        if (data != null) {
622            configure.addChild(data);
623        }
624        return packet;
625    }
626
627    public Iq bobResponse(Iq request) {
628        try {
629            final var bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid");
630            final var cid = BobTransfer.cid(bobCid);
631            final var f = mXmppConnectionService.getFileForCid(cid);
632            if (f == null || !f.canRead()) {
633                throw new IOException("No such file");
634            } else if (f.getSize() > 129000) {
635                final var response = request.generateResponse(Iq.Type.ERROR);
636                final var error = response.addChild("error");
637                error.setAttribute("type", "cancel");
638                error.addChild("policy-violation", "urn:ietf:params:xml:ns:xmpp-stanzas");
639                return response;
640            } else {
641                final var response = request.generateResponse(Iq.Type.RESULT);
642                final var data = response.addChild("data", "urn:xmpp:bob");
643                data.setAttribute("cid", bobCid);
644                data.setAttribute("type", f.getMimeType());
645                ByteArrayOutputStream b64 = new ByteArrayOutputStream((int) f.getSize() * 2);
646                Base64OutputStream b64wrap = new Base64OutputStream(b64, Base64.NO_WRAP);
647                ByteStreams.copy(new FileInputStream(f), b64wrap);
648                b64wrap.flush();
649                b64wrap.close();
650                data.setContent(b64.toString("utf-8"));
651                return response;
652            }
653        } catch (final IOException | IllegalStateException e) {
654            final var response = request.generateResponse(Iq.Type.ERROR);
655            final var error = response.addChild("error");
656            error.setAttribute("type", "cancel");
657            error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
658            return response;
659        }
660    }
661}