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