1package eu.siacs.conversations.xmpp.jingle;
  2
  3import com.google.common.collect.ImmutableMap;
  4import com.google.common.collect.Iterables;
  5import com.google.common.collect.Maps;
  6
  7import eu.siacs.conversations.xml.Element;
  8import eu.siacs.conversations.xml.Namespace;
  9import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 10import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 11import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 12import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 13import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 14import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
 15import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 16import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
 17import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
 18import eu.siacs.conversations.xmpp.jingle.transports.Transport;
 19import im.conversations.android.xmpp.model.jingle.Jingle;
 20
 21import java.util.Arrays;
 22import java.util.Collections;
 23import java.util.List;
 24import java.util.Map;
 25
 26public class FileTransferContentMap
 27        extends AbstractContentMap<FileTransferDescription, GenericTransportInfo> {
 28
 29    private static final List<Class<? extends GenericTransportInfo>> SUPPORTED_TRANSPORTS =
 30            Arrays.asList(
 31                    SocksByteStreamsTransportInfo.class,
 32                    IbbTransportInfo.class,
 33                    WebRTCDataChannelTransportInfo.class);
 34
 35    protected FileTransferContentMap(
 36            final Group group, final Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
 37                    contents) {
 38        super(group, contents);
 39    }
 40
 41    public static FileTransferContentMap of(final Jingle jinglePacket) {
 42        final Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
 43                contents = of(jinglePacket.getJingleContents());
 44        return new FileTransferContentMap(jinglePacket.getGroup(), contents);
 45    }
 46
 47    public static DescriptionTransport<FileTransferDescription, GenericTransportInfo> of(
 48            final Content content) {
 49        final GenericDescription description = content.getDescription();
 50        final GenericTransportInfo transportInfo = content.getTransport();
 51        final Content.Senders senders = content.getSenders();
 52        final FileTransferDescription fileTransferDescription;
 53        if (description == null) {
 54            fileTransferDescription = null;
 55        } else if (description instanceof FileTransferDescription ftDescription) {
 56            fileTransferDescription = ftDescription;
 57        } else {
 58            throw new UnsupportedApplicationException(
 59                    "Content does not contain file transfer description");
 60        }
 61        if (!SUPPORTED_TRANSPORTS.contains(transportInfo.getClass())) {
 62            throw new UnsupportedTransportException("Content does not have supported transport");
 63        }
 64        return new DescriptionTransport<>(senders, fileTransferDescription, transportInfo);
 65    }
 66
 67    private static Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
 68            of(final Map<String, Content> contents) {
 69        return ImmutableMap.copyOf(
 70                Maps.transformValues(contents, content -> content == null ? null : of(content)));
 71    }
 72
 73    public static FileTransferContentMap of(
 74            final FileTransferDescription.File file, final Transport.InitialTransportInfo initialTransportInfo) {
 75        // TODO copy groups
 76        final var transportInfo = initialTransportInfo.transportInfo;
 77        return new FileTransferContentMap(initialTransportInfo.group,
 78                Map.of(
 79                        initialTransportInfo.contentName,
 80                        new DescriptionTransport<>(
 81                                Content.Senders.INITIATOR,
 82                                FileTransferDescription.of(file),
 83                                transportInfo)));
 84    }
 85
 86    public FileTransferDescription.File requireOnlyFile() {
 87        if (this.contents.size() != 1) {
 88            throw new IllegalStateException("Only one file at a time is supported");
 89        }
 90        final var dt = Iterables.getOnlyElement(this.contents.values());
 91        return dt.description.getFile();
 92    }
 93
 94    public FileTransferDescription requireOnlyFileTransferDescription() {
 95        if (this.contents.size() != 1) {
 96            throw new IllegalStateException("Only one file at a time is supported");
 97        }
 98        final var dt = Iterables.getOnlyElement(this.contents.values());
 99        return dt.description;
100    }
101
102    public GenericTransportInfo requireOnlyTransportInfo() {
103        if (this.contents.size() != 1) {
104            throw new IllegalStateException(
105                    "We expect exactly one content with one transport info");
106        }
107        final var dt = Iterables.getOnlyElement(this.contents.values());
108        return dt.transport;
109    }
110
111    public FileTransferContentMap withTransport(final Transport.TransportInfo transportWrapper) {
112        final var transportInfo = transportWrapper.transportInfo;
113        return new FileTransferContentMap(transportWrapper.group,
114                ImmutableMap.copyOf(
115                        Maps.transformValues(
116                                contents,
117                                content -> {
118                                    if (content == null) {
119                                        return null;
120                                    }
121                                    return new DescriptionTransport<>(
122                                            content.senders, content.description, transportInfo);
123                                })));
124    }
125
126    public FileTransferContentMap candidateUsed(final String streamId, final String cid) {
127        return new FileTransferContentMap(null,
128                ImmutableMap.copyOf(
129                        Maps.transformValues(
130                                contents,
131                                content -> {
132                                    if (content == null) {
133                                        return null;
134                                    }
135                                    final var transportInfo =
136                                            new SocksByteStreamsTransportInfo(
137                                                    streamId, Collections.emptyList());
138                                    final Element candidateUsed =
139                                            transportInfo.addChild(
140                                                    "candidate-used",
141                                                    Namespace.JINGLE_TRANSPORTS_S5B);
142                                    candidateUsed.setAttribute("cid", cid);
143                                    return new DescriptionTransport<>(
144                                            content.senders, null, transportInfo);
145                                })));
146    }
147
148    public FileTransferContentMap candidateError(final String streamId) {
149        return new FileTransferContentMap(null,
150                ImmutableMap.copyOf(
151                        Maps.transformValues(
152                                contents,
153                                content -> {
154                                    if (content == null) {
155                                        return null;
156                                    }
157                                    final var transportInfo =
158                                            new SocksByteStreamsTransportInfo(
159                                                    streamId, Collections.emptyList());
160                                    transportInfo.addChild(
161                                            "candidate-error", Namespace.JINGLE_TRANSPORTS_S5B);
162                                    return new DescriptionTransport<>(
163                                            content.senders, null, transportInfo);
164                                })));
165    }
166
167    public FileTransferContentMap proxyActivated(final String streamId, final String cid) {
168        return new FileTransferContentMap(null,
169                ImmutableMap.copyOf(
170                        Maps.transformValues(
171                                contents,
172                                content -> {
173                                    if (content == null) {
174                                        return null;
175                                    }
176                                    final var transportInfo =
177                                            new SocksByteStreamsTransportInfo(
178                                                    streamId, Collections.emptyList());
179                                    final Element candidateUsed =
180                                            transportInfo.addChild(
181                                                    "activated", Namespace.JINGLE_TRANSPORTS_S5B);
182                                    candidateUsed.setAttribute("cid", cid);
183                                    return new DescriptionTransport<>(
184                                            content.senders, null, transportInfo);
185                                })));
186    }
187
188    FileTransferContentMap transportInfo() {
189        return new FileTransferContentMap(this.group,
190                Maps.transformValues(
191                        contents,
192                        dt -> new DescriptionTransport<>(dt.senders, null, dt.transport)));
193    }
194
195    FileTransferContentMap transportInfo(
196            final String contentName, final IceUdpTransportInfo.Candidate candidate) {
197        final DescriptionTransport<FileTransferDescription, GenericTransportInfo> descriptionTransport =
198                contents.get(contentName);
199        if (descriptionTransport == null) {
200            throw new IllegalArgumentException(
201                    "Unable to find transport info for content name " + contentName);
202        }
203        final WebRTCDataChannelTransportInfo transportInfo;
204        if (descriptionTransport.transport instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
205            transportInfo = webRTCDataChannelTransportInfo;
206        } else {
207            throw new IllegalStateException("TransportInfo is not WebRTCDataChannel");
208        }
209        final WebRTCDataChannelTransportInfo newTransportInfo = transportInfo.cloneWrapper();
210        newTransportInfo.addCandidate(candidate);
211        return new FileTransferContentMap(
212                null,
213                ImmutableMap.of(
214                        contentName,
215                        new DescriptionTransport<>(
216                                descriptionTransport.senders, null, newTransportInfo)));
217    }
218}