BlockingManager.java

  1package eu.siacs.conversations.xmpp.manager;
  2
  3import android.util.Log;
  4import androidx.annotation.NonNull;
  5import androidx.annotation.Nullable;
  6import com.google.common.collect.ImmutableSet;
  7import com.google.common.util.concurrent.FutureCallback;
  8import com.google.common.util.concurrent.Futures;
  9import com.google.common.util.concurrent.MoreExecutors;
 10import eu.siacs.conversations.Config;
 11import eu.siacs.conversations.entities.Blockable;
 12import eu.siacs.conversations.services.XmppConnectionService;
 13import eu.siacs.conversations.xml.Namespace;
 14import eu.siacs.conversations.xmpp.Jid;
 15import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 16import eu.siacs.conversations.xmpp.XmppConnection;
 17import im.conversations.android.xmpp.model.blocking.Block;
 18import im.conversations.android.xmpp.model.blocking.Blocklist;
 19import im.conversations.android.xmpp.model.blocking.Item;
 20import im.conversations.android.xmpp.model.blocking.Unblock;
 21import im.conversations.android.xmpp.model.error.Condition;
 22import im.conversations.android.xmpp.model.error.Error;
 23import im.conversations.android.xmpp.model.reporting.Report;
 24import im.conversations.android.xmpp.model.stanza.Iq;
 25import im.conversations.android.xmpp.model.unique.StanzaId;
 26import java.util.Collection;
 27import java.util.HashSet;
 28import java.util.Set;
 29
 30public class BlockingManager extends AbstractManager {
 31
 32    private final XmppConnectionService service;
 33
 34    private final HashSet<Jid> blocklist = new HashSet<>();
 35
 36    public BlockingManager(final XmppConnectionService service, final XmppConnection connection) {
 37        super(service, connection);
 38        // TODO find a way to get rid of XmppConnectionService and use context instead
 39        this.service = service;
 40    }
 41
 42    public void request() {
 43        final var future = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Blocklist()));
 44        Futures.addCallback(
 45                future,
 46                new FutureCallback<>() {
 47                    @Override
 48                    public void onSuccess(final Iq result) {
 49                        final var blocklist = result.getExtension(Blocklist.class);
 50                        if (blocklist == null) {
 51                            Log.d(
 52                                    Config.LOGTAG,
 53                                    getAccount().getJid().asBareJid()
 54                                            + ": invalid blocklist response");
 55                            return;
 56                        }
 57                        final var addresses = itemsAsAddresses(blocklist.getItems());
 58                        Log.d(
 59                                Config.LOGTAG,
 60                                getAccount().getJid().asBareJid()
 61                                        + ": discovered blocklist with "
 62                                        + addresses.size()
 63                                        + " items");
 64                        setBlocklist(addresses);
 65                        removeBlockedConversations(addresses);
 66                        service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
 67                    }
 68
 69                    @Override
 70                    public void onFailure(@NonNull final Throwable throwable) {
 71                        Log.w(
 72                                Config.LOGTAG,
 73                                getAccount().getJid().asBareJid()
 74                                        + ": could not retrieve blocklist",
 75                                throwable);
 76                    }
 77                },
 78                MoreExecutors.directExecutor());
 79    }
 80
 81    public void pushBlock(final Iq request) {
 82        if (connection.fromServer(request)) {
 83            final var block = request.getExtension(Block.class);
 84            final var addresses = itemsAsAddresses(block.getItems());
 85            synchronized (this.blocklist) {
 86                this.blocklist.addAll(addresses);
 87            }
 88            this.removeBlockedConversations(addresses);
 89            this.service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
 90            this.connection.sendResultFor(request);
 91        } else {
 92            this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
 93        }
 94    }
 95
 96    public void pushUnblock(final Iq request) {
 97        if (connection.fromServer(request)) {
 98            final var unblock = request.getExtension(Unblock.class);
 99            final var address = itemsAsAddresses(unblock.getItems());
100            synchronized (this.blocklist) {
101                this.blocklist.removeAll(address);
102            }
103            this.service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
104            this.connection.sendResultFor(request);
105        } else {
106            this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
107        }
108    }
109
110    private void removeBlockedConversations(final Collection<Jid> addresses) {
111        var removed = false;
112        for (final Jid address : addresses) {
113            removed |= service.removeBlockedConversations(getAccount(), address);
114        }
115        if (removed) {
116            service.updateConversationUi();
117        }
118    }
119
120    public ImmutableSet<Jid> getBlocklist() {
121        synchronized (this.blocklist) {
122            return ImmutableSet.copyOf(this.blocklist);
123        }
124    }
125
126    private void setBlocklist(final Collection<Jid> addresses) {
127        synchronized (this.blocklist) {
128            this.blocklist.clear();
129            this.blocklist.addAll(addresses);
130        }
131    }
132
133    public boolean hasFeature() {
134        return getManager(DiscoManager.class).hasServerFeature(Namespace.BLOCKING);
135    }
136
137    private static Set<Jid> itemsAsAddresses(final Collection<Item> items) {
138        final var builder = new ImmutableSet.Builder<Jid>();
139        for (final var item : items) {
140            final var jid = Jid.Invalid.getNullForInvalid(item.getJid());
141            if (jid == null) {
142                continue;
143            }
144            builder.add(jid);
145        }
146        return builder.build();
147    }
148
149    public boolean block(
150            @NonNull final Blockable blockable,
151            final boolean reportSpam,
152            @Nullable final String serverMsgId) {
153        final var address = blockable.getBlockedJid();
154        final var iq = new Iq(Iq.Type.SET);
155        final var block = iq.addExtension(new Block());
156        final var item = block.addExtension(new Item());
157        item.setJid(address);
158        if (reportSpam) {
159            final var report = item.addExtension(new Report());
160            report.setReason(Namespace.REPORTING_REASON_SPAM);
161            if (serverMsgId != null) {
162                // XEP has a 'by' attribute that is the same as reported jid but that doesn't make
163                // sense this the 'by' attribute in the stanza-id refers to the arriving entity
164                // (usually the account or the MUC)
165                report.addExtension(new StanzaId(serverMsgId));
166            }
167        }
168        final var future = this.connection.sendIqPacket(iq);
169        Futures.addCallback(
170                future,
171                new FutureCallback<>() {
172                    @Override
173                    public void onSuccess(Iq result) {
174                        synchronized (blocklist) {
175                            blocklist.add(address);
176                        }
177                        service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
178                    }
179
180                    @Override
181                    public void onFailure(@NonNull Throwable throwable) {
182                        Log.d(
183                                Config.LOGTAG,
184                                getAccount().getJid().asBareJid() + ": could not block " + address,
185                                throwable);
186                    }
187                },
188                MoreExecutors.directExecutor());
189        if (address.isFullJid()) {
190            return false;
191        } else if (service.removeBlockedConversations(getAccount(), address)) {
192            service.updateConversationUi();
193            return true;
194        } else {
195            return false;
196        }
197    }
198
199    public void unblock(@NonNull final Blockable blockable) {
200        final var address = blockable.getBlockedJid();
201        final var iq = new Iq(Iq.Type.SET);
202        final var unblock = iq.addExtension(new Unblock());
203        final var item = unblock.addExtension(new Item());
204        item.setJid(address);
205        final var future = this.connection.sendIqPacket(iq);
206        Futures.addCallback(
207                future,
208                new FutureCallback<Iq>() {
209                    @Override
210                    public void onSuccess(Iq result) {
211                        synchronized (blocklist) {
212                            blocklist.remove(address);
213                        }
214                        service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
215                    }
216
217                    @Override
218                    public void onFailure(@NonNull Throwable t) {
219                        Log.d(
220                                Config.LOGTAG,
221                                getAccount().getJid().asBareJid()
222                                        + ": could not unblock "
223                                        + address,
224                                t);
225                    }
226                },
227                MoreExecutors.directExecutor());
228    }
229}