]> nmode's Git Repositories - signal-cli/blob - src/main/java/cli/Main.java
Implement quitGroups
[signal-cli] / src / main / java / cli / Main.java
1 /**
2 * Copyright (C) 2015 AsamK
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 package cli;
18
19 import net.sourceforge.argparse4j.ArgumentParsers;
20 import net.sourceforge.argparse4j.impl.Arguments;
21 import net.sourceforge.argparse4j.inf.*;
22 import org.apache.commons.io.IOUtils;
23 import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
24 import org.whispersystems.textsecure.api.messages.*;
25 import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
26 import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
27 import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
28 import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
29
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.nio.file.Paths;
36 import java.security.Security;
37 import java.util.ArrayList;
38 import java.util.List;
39
40 public class Main {
41
42 public static void main(String[] args) {
43 // Workaround for BKS truststore
44 Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
45
46 Namespace ns = parseArgs(args);
47 if (ns == null) {
48 System.exit(1);
49 }
50
51 final String username = ns.getString("username");
52 final Manager m = new Manager(username);
53 if (m.userExists()) {
54 try {
55 m.load();
56 } catch (Exception e) {
57 System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage());
58 System.exit(2);
59 }
60 }
61
62 switch (ns.getString("command")) {
63 case "register":
64 if (!m.userHasKeys()) {
65 m.createNewIdentity();
66 }
67 try {
68 m.register(ns.getBoolean("voice"));
69 } catch (IOException e) {
70 System.err.println("Request verify error: " + e.getMessage());
71 System.exit(3);
72 }
73 break;
74 case "verify":
75 if (!m.userHasKeys()) {
76 System.err.println("User has no keys, first call register.");
77 System.exit(1);
78 }
79 if (m.isRegistered()) {
80 System.err.println("User registration is already verified");
81 System.exit(1);
82 }
83 try {
84 m.verifyAccount(ns.getString("verificationCode"));
85 } catch (IOException e) {
86 System.err.println("Verify error: " + e.getMessage());
87 System.exit(3);
88 }
89 break;
90 case "send":
91 if (!m.isRegistered()) {
92 System.err.println("User is not registered.");
93 System.exit(1);
94 }
95
96 byte[] groupId = null;
97 List<String> recipients = null;
98 if (ns.getString("group") != null) {
99 try {
100 GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group")));
101 if (g == null) {
102 System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": Unknown group");
103 System.err.println("Aborting sending.");
104 System.exit(1);
105 }
106 groupId = g.groupId;
107 recipients = g.members;
108 } catch (IOException e) {
109 System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage());
110 System.err.println("Aborting sending.");
111 System.exit(1);
112 }
113 } else {
114 recipients = ns.<String>getList("recipient");
115 }
116
117 if (ns.getBoolean("endsession")) {
118 sendEndSessionMessage(m, recipients);
119 } else {
120 final List<String> attachments = ns.getList("attachment");
121 List<TextSecureAttachment> textSecureAttachments = null;
122 if (attachments != null) {
123 textSecureAttachments = new ArrayList<>(attachments.size());
124 for (String attachment : attachments) {
125 try {
126 File attachmentFile = new File(attachment);
127 InputStream attachmentStream = new FileInputStream(attachmentFile);
128 final long attachmentSize = attachmentFile.length();
129 String mime = Files.probeContentType(Paths.get(attachment));
130 textSecureAttachments.add(new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null));
131 } catch (IOException e) {
132 System.err.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage());
133 System.err.println("Aborting sending.");
134 System.exit(1);
135 }
136 }
137 }
138
139 String messageText = ns.getString("message");
140 if (messageText == null) {
141 try {
142 messageText = IOUtils.toString(System.in);
143 } catch (IOException e) {
144 System.err.println("Failed to read message from stdin: " + e.getMessage());
145 System.err.println("Aborting sending.");
146 System.exit(1);
147 }
148 }
149
150 sendMessage(m, messageText, textSecureAttachments, recipients, groupId);
151 }
152
153 break;
154 case "receive":
155 if (!m.isRegistered()) {
156 System.err.println("User is not registered.");
157 System.exit(1);
158 }
159 int timeout = 5;
160 if (ns.getInt("timeout") != null) {
161 timeout = ns.getInt("timeout");
162 }
163 boolean returnOnTimeout = true;
164 if (timeout < 0) {
165 returnOnTimeout = false;
166 timeout = 3600;
167 }
168 try {
169 m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m));
170 } catch (IOException e) {
171 System.err.println("Error while receiving message: " + e.getMessage());
172 System.exit(3);
173 } catch (AssertionError e) {
174 System.err.println("Failed to receive message (Assertion): " + e.getMessage());
175 System.err.println(e.getStackTrace());
176 System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
177 System.exit(1);
178 }
179 break;
180 case "quitGroup":
181 if (!m.isRegistered()) {
182 System.err.println("User is not registered.");
183 System.exit(1);
184 }
185
186 try {
187 GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group")));
188 if (g == null) {
189 System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": Unknown group");
190 System.err.println("Aborting sending.");
191 System.exit(1);
192 }
193
194 sendQuitGroupMessage(m, g.members, g.groupId);
195 } catch (IOException e) {
196 System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage());
197 System.err.println("Aborting sending.");
198 System.exit(1);
199 }
200
201 break;
202 }
203 m.save();
204 System.exit(0);
205 }
206
207 private static Namespace parseArgs(String[] args) {
208 ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli")
209 .defaultHelp(true)
210 .description("Commandline interface for TextSecure.")
211 .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION);
212
213 parser.addArgument("-u", "--username")
214 .help("Specify your phone number, that will be used for verification.");
215 parser.addArgument("-v", "--version")
216 .help("Show package version.")
217 .action(Arguments.version());
218
219 Subparsers subparsers = parser.addSubparsers()
220 .title("subcommands")
221 .dest("command")
222 .description("valid subcommands")
223 .help("additional help");
224
225 Subparser parserRegister = subparsers.addParser("register");
226 parserRegister.addArgument("-v", "--voice")
227 .help("The verification should be done over voice, not sms.")
228 .action(Arguments.storeTrue());
229
230 Subparser parserVerify = subparsers.addParser("verify");
231 parserVerify.addArgument("verificationCode")
232 .help("The verification code you received via sms or voice call.");
233
234 Subparser parserSend = subparsers.addParser("send");
235 parserSend.addArgument("-g", "--group")
236 .help("Specify the recipient group ID.");
237 parserSend.addArgument("recipient")
238 .help("Specify the recipients' phone number.")
239 .nargs("*");
240 parserSend.addArgument("-m", "--message")
241 .help("Specify the message, if missing standard input is used.");
242 parserSend.addArgument("-a", "--attachment")
243 .nargs("*")
244 .help("Add file as attachment");
245 parserSend.addArgument("-e", "--endsession")
246 .help("Clear session state and send end session message.")
247 .action(Arguments.storeTrue());
248
249 Subparser parserLeaveGroup = subparsers.addParser("quitGroup");
250 parserLeaveGroup.addArgument("-g", "--group")
251 .required(true)
252 .help("Specify the recipient group ID.");
253
254 Subparser parserReceive = subparsers.addParser("receive");
255 parserReceive.addArgument("-t", "--timeout")
256 .type(int.class)
257 .help("Number of seconds to wait for new messages (negative values disable timeout)");
258
259 try {
260 Namespace ns = parser.parseArgs(args);
261 if (ns.getString("username") == null) {
262 parser.printUsage();
263 System.err.println("You need to specify a username (phone number)");
264 System.exit(2);
265 }
266 if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
267 System.err.println("You cannot specify recipients by phone number and groups a the same time");
268 System.exit(2);
269 }
270 return ns;
271 } catch (ArgumentParserException e) {
272 parser.handleError(e);
273 return null;
274 }
275 }
276
277 private static void sendMessage(Manager m, String messageText, List<TextSecureAttachment> textSecureAttachments,
278 List<String> recipients, byte[] groupId) {
279 final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText);
280 if (textSecureAttachments != null) {
281 messageBuilder.withAttachments(textSecureAttachments);
282 }
283 if (groupId != null) {
284 messageBuilder.asGroupMessage(new TextSecureGroup(groupId));
285 }
286 TextSecureDataMessage message = messageBuilder.build();
287
288 sendMessage(m, message, recipients);
289 }
290
291 private static void sendEndSessionMessage(Manager m, List<String> recipients) {
292 final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().asEndSessionMessage();
293
294 TextSecureDataMessage message = messageBuilder.build();
295
296 sendMessage(m, message, recipients);
297 }
298
299 private static void sendQuitGroupMessage(Manager m, List<String> recipients, byte[] groupId) {
300 final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder();
301 TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT).withId(groupId).build();
302 messageBuilder.asGroupMessage(group);
303
304 TextSecureDataMessage message = messageBuilder.build();
305
306 sendMessage(m, message, recipients);
307 }
308
309 private static void sendMessage(Manager m, TextSecureDataMessage message, List<String> recipients) {
310 try {
311 m.sendMessage(recipients, message);
312 } catch (IOException e) {
313 System.err.println("Failed to send message: " + e.getMessage());
314 } catch (EncapsulatedExceptions e) {
315 System.err.println("Failed to send (some) messages:");
316 for (NetworkFailureException n : e.getNetworkExceptions()) {
317 System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
318 }
319 for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
320 System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
321 }
322 for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
323 System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage());
324 }
325 } catch (AssertionError e) {
326 System.err.println("Failed to send message (Assertion): " + e.getMessage());
327 System.err.println(e.getStackTrace());
328 System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
329 System.exit(1);
330 }
331 }
332
333 private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
334 final Manager m;
335
336 public ReceiveMessageHandler(Manager m) {
337 this.m = m;
338 }
339
340 @Override
341 public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) {
342 System.out.println("Envelope from: " + envelope.getSource());
343 System.out.println("Timestamp: " + envelope.getTimestamp());
344
345 if (envelope.isReceipt()) {
346 System.out.println("Got receipt.");
347 } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) {
348 if (content == null) {
349 System.out.println("Failed to decrypt message.");
350 } else {
351 if (content.getDataMessage().isPresent()) {
352 TextSecureDataMessage message = content.getDataMessage().get();
353
354 System.out.println("Message timestamp: " + message.getTimestamp());
355
356 if (message.getBody().isPresent()) {
357 System.out.println("Body: " + message.getBody().get());
358 }
359 if (message.getGroupInfo().isPresent()) {
360 TextSecureGroup groupInfo = message.getGroupInfo().get();
361 System.out.println("Group info:");
362 System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
363 if (groupInfo.getName().isPresent()) {
364 System.out.println(" Name: " + groupInfo.getName().get());
365 } else if (group != null) {
366 System.out.println(" Name: " + group.name);
367 } else {
368 System.out.println(" Name: <Unknown group>");
369 }
370 System.out.println(" Type: " + groupInfo.getType());
371 if (groupInfo.getMembers().isPresent()) {
372 for (String member : groupInfo.getMembers().get()) {
373 System.out.println(" Member: " + member);
374 }
375 }
376 if (groupInfo.getAvatar().isPresent()) {
377 System.out.println(" Avatar:");
378 printAttachment(groupInfo.getAvatar().get());
379 }
380 }
381 if (message.isEndSession()) {
382 System.out.println("Is end session");
383 }
384
385 if (message.getAttachments().isPresent()) {
386 System.out.println("Attachments: ");
387 for (TextSecureAttachment attachment : message.getAttachments().get()) {
388 printAttachment(attachment);
389 }
390 }
391 }
392 if (content.getSyncMessage().isPresent()) {
393 TextSecureSyncMessage syncMessage = content.getSyncMessage().get();
394 System.out.println("Received sync message");
395 }
396 }
397 } else {
398 System.out.println("Unknown message received.");
399 }
400 System.out.println();
401 }
402
403 private void printAttachment(TextSecureAttachment attachment) {
404 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
405 if (attachment.isPointer()) {
406 final TextSecureAttachmentPointer pointer = attachment.asPointer();
407 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
408 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
409 File file = m.getAttachmentFile(pointer.getId());
410 if (file.exists()) {
411 System.out.println(" Stored plaintext in: " + file);
412 }
413 }
414 }
415 }
416 }