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}