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}