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