1 /*
2  * Copyright (C) 2021 Finn Herzfeld
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 package io.finn.signald.clientprotocol.v1;
19 
20 import static io.finn.signald.annotations.ExactlyOneOfRequired.RECIPIENT;
21 
22 import io.finn.signald.JsonAttachment;
23 import io.finn.signald.Manager;
24 import io.finn.signald.annotations.*;
25 import io.finn.signald.clientprotocol.Request;
26 import io.finn.signald.clientprotocol.RequestType;
27 import io.finn.signald.clientprotocol.v1.exceptions.*;
28 import io.finn.signald.clientprotocol.v1.exceptions.InternalError;
29 import io.finn.signald.db.Recipient;
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.nio.file.Files;
35 import java.sql.SQLException;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.stream.Collectors;
39 import org.whispersystems.libsignal.util.guava.Optional;
40 import org.whispersystems.signalservice.api.messages.SendMessageResult;
41 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
42 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
43 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
44 
45 @ProtocolType("send")
46 public class SendRequest implements RequestType<SendResponse> {
47   @ExampleValue(ExampleValue.LOCAL_PHONE_NUMBER) @Required public String username;
48   @ExactlyOneOfRequired(RECIPIENT) public JsonAddress recipientAddress;
49   @ExampleValue(ExampleValue.GROUP_ID) @ExactlyOneOfRequired(RECIPIENT) public String recipientGroupId;
50   @ExampleValue(ExampleValue.MESSAGE_BODY) @AtLeastOneOfRequired({"attachments"}) public String messageBody;
51   @AtLeastOneOfRequired({"messageBody"}) public List<JsonAttachment> attachments;
52   public JsonQuote quote;
53   public Long timestamp;
54   public List<JsonMention> mentions;
55   public List<JsonPreview> previews;
56 
57   @Override
run(Request request)58   public SendResponse run(Request request)
59       throws NoSuchAccountError, ServerNotFoundError, InvalidProxyError, NoSendPermissionError, InvalidAttachmentError, InternalError, UnknownGroupError, InvalidRecipientError {
60     Manager manager = Common.getManager(username);
61 
62     SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder();
63 
64     Recipient recipient = null;
65     if (recipientAddress != null) {
66       try {
67         recipient = manager.getRecipientsTable().get(recipientAddress);
68       } catch (IOException | SQLException e) {
69         throw new InternalError("error looking up recipient", e);
70       }
71     }
72 
73     if (messageBody != null) {
74       messageBuilder = messageBuilder.withBody(messageBody);
75     }
76 
77     if (attachments != null) {
78       List<SignalServiceAttachment> signalServiceAttachments = new ArrayList<>(attachments.size());
79       for (JsonAttachment attachment : attachments) {
80         try {
81           File attachmentFile = new File(attachment.filename);
82           InputStream attachmentStream = new FileInputStream(attachmentFile);
83           final long attachmentSize = attachmentFile.length();
84           if (attachment.contentType == null) {
85             attachment.contentType = Files.probeContentType(attachmentFile.toPath());
86             if (attachment.contentType == null) {
87               attachment.contentType = "application/octet-stream";
88             }
89           }
90           String customFilename = attachmentFile.getName();
91           if (attachment.customFilename != null) {
92             customFilename = attachment.customFilename;
93           }
94           signalServiceAttachments.add(new SignalServiceAttachmentStream(
95               attachmentStream, attachment.contentType, attachmentSize, Optional.of(customFilename), attachment.voiceNote, false, false, attachment.getPreview(), attachment.width,
96               attachment.height, System.currentTimeMillis(), Optional.fromNullable(attachment.caption), Optional.fromNullable(attachment.blurhash), null, null, Optional.absent()));
97         } catch (IOException e) {
98           throw new InvalidAttachmentError(attachment.filename, e);
99         }
100       }
101       messageBuilder.withAttachments(signalServiceAttachments);
102     }
103 
104     if (quote != null) {
105       messageBuilder.withQuote(quote.getQuote());
106     }
107 
108     if (timestamp == null) {
109       timestamp = System.currentTimeMillis();
110     }
111     messageBuilder.withTimestamp(timestamp);
112 
113     if (mentions != null && mentions.size() > 0) {
114       messageBuilder.withMentions(mentions.stream().map(JsonMention::asMention).collect(Collectors.toList()));
115     }
116 
117     if (previews != null) {
118       List<SignalServiceDataMessage.Preview> signalPreviews = new ArrayList<>();
119       for (JsonPreview preview : previews) {
120         signalPreviews.add(preview.asSignalPreview());
121       }
122       messageBuilder.withPreviews(signalPreviews);
123     }
124 
125     List<SendMessageResult> results;
126 
127     results = Common.send(manager, messageBuilder, recipient, recipientGroupId);
128     return new SendResponse(results, timestamp);
129   }
130 }
131