diff --git a/.gitignore b/.gitignore index 3dc9875b..f609c19f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ local.properties .project .settings/ out/ +.vscode +testme +buildcontainer diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..18b8d677 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM openjdk:8u151-alpine +RUN apk update \ +&& apk upgrade \ +&& apk add --no-cache bash libc6-compat +COPY build/install/signal-cli /opt/signal-cli +ENV PATH="/opt/signal-cli/bin:${PATH}" +RUN adduser -D -g '' user +USER user +RUN mkdir -pv ~/.config/signal/data +CMD ["signal-cli", "--singleuser", "socket", "-a", "0.0.0.0"] diff --git a/README.md b/README.md index 31a12c94..0dbd837b 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ You can find further instructions on the Wiki: - [Install on Ubuntu](https://github.com/AsamK/signal-cli/wiki/HowToUbuntu) - [DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service) +- [Socket IPC using JSON](https://github.com/Matteljay/signal-cli/wiki/Socket.md) ## Usage diff --git a/SOCKET.md b/SOCKET.md new file mode 100644 index 00000000..02d2f2a8 --- /dev/null +++ b/SOCKET.md @@ -0,0 +1,152 @@ +# Socket IPC with signal-cli + +Signal-cli offers a simple yet powerful way of inter-process communication via sockets. The purpose is to be able to connect your own programmed logic to Signal. Allowing you to manage contacts and their privileges on your own so you can create a powerful automated chat bot with Signal. It is now easy to set up signal-cli in it's own Docker microservice and connect a script to it running in a separate container. This configuration adds security and deployment speed via Docker's network orchestration. There is a [Dockerfile](/Matteljay/signal-cli/blob/master/Dockerfile) included. + +## Get started + +The API formats are explained below but first, signal-cli should be launched as a socket daemon. As an alternative to Docker, the command below could be wrapped in a [systemd](https://www.shellhacks.com/systemd-service-file-example/) or [screen](https://www.thegeekdiary.com/how-to-use-the-screen-command-in-linux/) launcher script. +It is assumed you have one Signal user registered and verified as explained in the [README](/Matteljay/signal-cli/blob/master/README.md#usage) file. You are now ready to launch the socket listener from the command line: + + signal-cli --singleuser socket + +Or by setting the default values explicitly: + + signal-cli -u USERNAME socket -a localhost -p 24250 + +This will start listening and sending JSON string objects locally on port `24250`. From your own bash or python program, you can now interact seamlessly with the Signal server via this socket. An example of a single-threaded non-blocking testing application can be found here [socketTest.py](/Matteljay/signal-cli/blob/master/src/socketTest.py). + +## Sending + +``` +{ + "sendMessage" : { + "contacts" : [ "+31638555555" ], + "message" : "This PM comes from Python <3" + } +} + +{ + "sendMessage" : { + "groups" : [ "Y5555rtl2p/TnLYvY555dA==", "DK555555UjPU55545557bA==" ], + "message" : "This GROUP message comes from Python!" + } +} + +{ + "updateContacts" : { + "+31638555555" : { + "name" : "NewName", + "color" : "", + "messageExpirationTime" : 555000, + "blocked" : "false", + "inboxPosition" : 2, + "archived" : true + } + } +} + +{ + "trust" : { + "contacts" : [ "+31638555555" ] + } +} + +{ + "endSession" : { + "contacts" : [ "+31638555555" ] + } +} + +{ + "getContacts" : "" +} + +{ + "getGroups" : "" +} +``` + +## Receiving + +### Incoming message + +``` +{ + "envelope": { + "source": "+31638422555", + "sourceDevice": 1, + "relay": None, + "timestamp": 1592692535999, + "isReceipt": False, + "dataMessage": { + "timestamp": 1592692535999, + "message": "Hello signal-cli!", + "expiresInSeconds": 0, + "attachments": [], + "groupInfo": { + "groupId": "Y5555rtl2p/TnLYvY555dA==", + "members": None, + "name": None, + "type": "DELIVER" + } + }, + "syncMessage": None, + "callMessage": None, + "receiptMessage": None + } +} +``` + +### Response to 'getContacts' + +``` +{ + "+31638422555" : { + "name" : null, + "uuid" : "1555f555-eb02-555d-a1b6-555517905552", + "color" : null, + "messageExpirationTime" : 0, + "profileKey" : "Y4555c7P4LM6Y555XW2PKx4555BMFxO555g/sJ555KU=", + "blocked" : false, + "inboxPosition" : null, + "archived" : false + }, + "+31638422999" : { + "name" : null, + "uuid" : "1999f999-eb02-999d-a1b6-999517909992", + "color" : null, + "messageExpirationTime" : 0, + "profileKey" : "Y4999c7P4LM6Y999XW2PKx4999BMFxO999g/sJ999KU=", + "blocked" : false, + "inboxPosition" : null, + "archived" : false + } +} +``` + +### Response to 'getGroups' + +``` +{ + "DK555555UjPU55545557bA==" : { + "name" : "Watchdogs inc.", + "color" : null, + "messageExpirationTime" : 0, + "blocked" : false, + "inboxPosition" : null, + "archived" : false, + "avatarId" : 0, + "members" : [ "+31638555555", "+31638999999" ] + }, + "Y5555rtl2p/TnLYvY555dA==" : { + "name" : "Freddy ftw", + "color" : null, + "messageExpirationTime" : 0, + "blocked" : false, + "inboxPosition" : null, + "archived" : false, + "avatarId" : 0, + "members" : [ "+31638555555", "+31638999999" ] + } +} +``` diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 5aa57f44..153c1893 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -40,8 +40,12 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content)); } try { - jsonProcessor.writeValue(System.out, result); - System.out.println(); + String output = jsonProcessor.writeValueAsString(result); + if (m.socketTasks != null) { + m.socketTasks.send(output); + } else { + System.out.println(output); + } } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index b95b0093..1a878455 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -26,6 +26,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparsers; import org.asamk.Signal; +import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.commands.Command; import org.asamk.signal.commands.Commands; import org.asamk.signal.commands.DbusCommand; @@ -36,6 +37,7 @@ import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.ServiceConfig; +import org.asamk.signal.manager.PathConfig; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -73,7 +75,7 @@ public class Main { } private static int handleCommands(Namespace ns) { - final String username = ns.getString("username"); + String username = ns.getString("username"); if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { try { @@ -103,6 +105,16 @@ public class Main { dataPath = getDefaultDataPath(); } + if (ns.getBoolean("singleuser")) { + String completeDataPath = PathConfig.createDefault(dataPath).getDataPath(); + username = SignalAccount.getSingleUser(completeDataPath); + if (username == null) { + System.exit(1); + } + System.out.println("Assuming username: " + username); + ns.getAttrs().put("username", username); + } + final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT); if (username == null) { @@ -234,6 +246,9 @@ public class Main { MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); mut.addArgument("-u", "--username") .help("Specify your phone number, that will be used for verification."); + mut.addArgument("--singleuser") + .help("Can be used if only one user account exists on this machine.") + .action(Arguments.storeTrue()); mut.addArgument("--dbus") .help("Make request via user dbus.") .action(Arguments.storeTrue()); @@ -267,7 +282,7 @@ public class Main { System.err.println("You cannot specify a username (phone number) when linking"); System.exit(2); } - } else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { + } else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system") && !ns.getBoolean("singleuser")) { if (ns.getString("username") == null) { parser.printUsage(); System.err.println("You need to specify a username (phone number)"); diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 183b40a0..cbd82cb5 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -34,6 +34,7 @@ public class Commands { addCommand("updateProfile", new UpdateProfileCommand()); addCommand("verify", new VerifyCommand()); addCommand("uploadStickerPack", new UploadStickerPackCommand()); + addCommand("socket", new SocketCommand()); } public static Map getCommands() { diff --git a/src/main/java/org/asamk/signal/commands/SocketCommand.java b/src/main/java/org/asamk/signal/commands/SocketCommand.java new file mode 100644 index 00000000..08fc19ae --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SocketCommand.java @@ -0,0 +1,65 @@ +package org.asamk.signal.commands; + +import java.util.concurrent.TimeUnit; +import java.io.IOException; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.manager.Manager; +import org.asamk.signal.JsonReceiveMessageHandler; +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; + +import org.asamk.signal.socket.SocketTasks; + +public class SocketCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-a", "--address") + .setDefault("127.0.0.1") + .help("Socket bind address"); + subparser.addArgument("-p", "--port") + .type(int.class) + .setDefault(24250) + .help("Socket port to use"); + subparser.help("Receive at a socket while being able to send as well"); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + System.out.println("Starting socket thread..."); + m.socketTasks = new SocketTasks(ns, m); + try { + Thread thread = new Thread(m.socketTasks); + thread.start(); + } catch (Exception e) { + System.err.println(e); + } + try { + // Let the thread settle for a second + TimeUnit.SECONDS.sleep(1); + } catch (Exception e) {} + System.out.println("Listening from Signal server..."); + + final long timeout = 3600 * 1000; // timeout in ms + final boolean returnOnTimeout = false; + final boolean ignoreAttachments = true; + try { + final Manager.ReceiveMessageHandler handler = new JsonReceiveMessageHandler(m); + while(true) { + m.receiveMessages(timeout, TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, handler); + } + } catch (IOException e) { + System.err.println("Error while receiving messages: " + e.getMessage()); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index e57bd531..b896b0b1 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -141,6 +141,8 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.asamk.signal.socket.SocketTasks; + public class Manager implements Closeable { private final SleepTimer timer = new UptimeSleepTimer(); @@ -153,6 +155,8 @@ public class Manager implements Closeable { private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; + public SocketTasks socketTasks = null; + public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) { this.account = account; this.pathConfig = pathConfig; @@ -163,6 +167,10 @@ public class Manager implements Closeable { this.account.setResolver(this::resolveSignalServiceAddress); } + public SignalAccount getAccount() { + return account; + } + public String getUsername() { return account.getUsername(); } diff --git a/src/main/java/org/asamk/signal/socket/Commander.java b/src/main/java/org/asamk/signal/socket/Commander.java new file mode 100644 index 00000000..18f51996 --- /dev/null +++ b/src/main/java/org/asamk/signal/socket/Commander.java @@ -0,0 +1,287 @@ +package org.asamk.signal.socket; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.io.IOException; +import java.net.Socket; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.core.type.TypeReference; + +import org.asamk.signal.storage.SignalAccount; +import org.asamk.signal.storage.contacts.ContactInfo; +import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.Util; +import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.whispersystems.util.Base64; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; +import org.asamk.signal.socket.SocketTasks; + +public class Commander { + private Manager m; + private SocketTasks tasks; + private ObjectMapper mapper; + + public Commander(Manager manager, SocketTasks socketTasks) { + this.m = manager; + this.tasks = socketTasks; + this.mapper = new ObjectMapper(); + } + + public void handleCommand(String command, JsonNode arguments) { + switch (command) { + case "getContacts": + this.tasks.send(this.getContacts()); + break; + case "updateContacts": + this.updateContacts(arguments); + break; + case "sendMessage": + this.sendMessage(arguments); + break; + case "trust": + this.trust(arguments); + break; + case "endSession": + this.endSession(arguments); + break; + case "getGroups": + this.tasks.send(this.getGroups()); + break; + default: + this.tasks.send("Unknown command: '" + command + "'"); + } + } + + private String getGroups() { + ObjectNode node = this.mapper.createObjectNode(); + List groups = this.m.getAccount().getGroupStore().getGroups(); + for (GroupInfo g: groups) { + ObjectNode group = this.mapper.createObjectNode(); + group.put("name", g.name); + group.put("color", g.color); + group.put("messageExpirationTime", g.messageExpirationTime); + group.put("blocked", g.blocked); + group.put("inboxPosition", g.inboxPosition); + group.put("archived", g.archived); + group.put("avatarId", g.getAvatarId()); + for (String m: g.getMembersE164()) { + group.putArray("members").add(m); + } + node.set(Base64.encodeBytes(g.groupId), group); + } + String out = null; + try { + out = this.mapper.writeValueAsString(node); //writerWithDefaultPrettyPrinter() + } catch (Exception e) { + System.err.println("Could not send groups"); + } + return out; + } + + private void sendMessage(JsonNode args) { + Iterator> entryIt = args.fields(); + Map.Entry entry; + String argument; + String message = null; + ArrayList contacts = null; + ArrayList groups = null; + while (entryIt.hasNext()) { + entry = entryIt.next(); + argument = entry.getKey(); + switch(argument) { + case "contacts": + try { + contacts = this.mapper.readValue(entry.getValue().toString(), new TypeReference>(){}); + } catch (IOException e) { + System.err.println("bad list of contacts"); + return; + } + break; + case "groups": + try { + groups = this.mapper.readValue(entry.getValue().toString(), new TypeReference>(){}); + } catch (IOException e) { + System.err.println("bad list of groups"); + return; + } + break; + case "message": + message = entry.getValue().asText(); + if (message.trim().length() <= 0) { + System.err.println("cannot send empty message"); + return; + } + break; + } + } + if (contacts != null) { + try { + this.m.sendMessage(message, new ArrayList<>(), contacts); + } catch (Exception | EncapsulatedExceptions | InvalidNumberException e) { + System.err.println(e); + } + } + if (groups != null) { + byte[] groupId; + for (String group: groups) { + try { + groupId = Util.decodeGroupId(group); + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + continue; + } + try { + this.m.sendGroupMessage(message, new ArrayList<>(), groupId); + } catch (IOException | EncapsulatedExceptions | GroupNotFoundException | AttachmentInvalidException | NotAGroupMemberException e) { + System.err.println(e); + } + } + } + } + + private void endSession(JsonNode args) { + Iterator> entryIt = args.fields(); + Map.Entry entry; + String argument; + ArrayList contacts = null; + while (entryIt.hasNext()) { + entry = entryIt.next(); + argument = entry.getKey(); + switch(argument) { + case "contacts": + try { + contacts = this.mapper.readValue(entry.getValue().toString(), new TypeReference>(){}); + } catch (IOException e) { + System.err.println("bad list of contacts"); + return; + } + break; + } + } + if (contacts != null) { + try { + this.m.sendEndSessionMessage(contacts); + System.err.println("WARNING: Ended session with " + String.join(", ", contacts)); + } catch (Exception | EncapsulatedExceptions | InvalidNumberException e) { + System.err.println(e); + } + } + } + + private void trust(JsonNode args) { + Iterator> entryIt = args.fields(); + Map.Entry entry; + String argument; + ArrayList contacts = null; + while (entryIt.hasNext()) { + entry = entryIt.next(); + argument = entry.getKey(); + switch(argument) { + case "contacts": + try { + contacts = this.mapper.readValue(entry.getValue().toString(), new TypeReference>(){}); + } catch (IOException e) { + System.err.println("bad list of contacts"); + return; + } + break; + } + } + if (contacts != null) { + for (String contact: contacts) { + this.m.trustIdentityAllKeys(contact); + } + System.err.println("WARNING: Updated trust for all keys from " + String.join(", ", contacts)); + } + } + + private String getContacts() { + ObjectNode node = this.mapper.createObjectNode(); + List contacts = this.m.getContacts(); + for (ContactInfo c: contacts) { + ObjectNode group = this.mapper.createObjectNode(); + group.put("name", c.name); + group.put("uuid", c.uuid.toString()); + group.put("color", c.color); + group.put("messageExpirationTime", c.messageExpirationTime); + group.put("profileKey", c.profileKey); + group.put("blocked", c.blocked); + group.put("inboxPosition", c.inboxPosition); + group.put("archived", c.archived); + node.set(c.number, group); + } + String out = null; + try { + out = this.mapper.writeValueAsString(node); //writerWithDefaultPrettyPrinter() + } catch (Exception e) { + System.err.println("Could not send contacts"); + } + return out; + } + + private void updateContacts(JsonNode args) { + Iterator> entryIt = args.fields(); + Map.Entry entry; + String number; + while (entryIt.hasNext()) { + entry = entryIt.next(); + number = entry.getKey(); + ContactInfo contactInfo = this.m.getContact(number); + if (contactInfo == null) { + System.err.println("Could not update " + number); + continue; + } else { + patchContactInfo(contactInfo, entry.getValue()); + SignalAccount account = this.m.getAccount(); + account.getContactStore().updateContact(contactInfo); + account.save(); + System.out.println("Updated account info for " + number); + } + } + } + + private void patchContactInfo(ContactInfo contactInfo, JsonNode args) { + Iterator> entryIt = args.fields(); + Map.Entry entry; + String argument; + JsonNode value; + while (entryIt.hasNext()) { + entry = entryIt.next(); + argument = entry.getKey(); + value = entry.getValue(); + switch (argument) { + case "name": + contactInfo.name = value.asText(); + break; + case "color": + contactInfo.color = value.asText(); + break; + case "messageExpirationTime": + contactInfo.messageExpirationTime = value.asInt(); + break; + case "blocked": + contactInfo.blocked = value.asBoolean(); + break; + case "inboxPosition": + contactInfo.inboxPosition = value.asInt(); + break; + case "archived": + contactInfo.archived = value.asBoolean(); + break; + default: + System.err.println("Invalid field '" + argument + "' in " + contactInfo.number); + } + } + } + +} diff --git a/src/main/java/org/asamk/signal/socket/SocketTasks.java b/src/main/java/org/asamk/signal/socket/SocketTasks.java new file mode 100644 index 00000000..0edb1ab3 --- /dev/null +++ b/src/main/java/org/asamk/signal/socket/SocketTasks.java @@ -0,0 +1,102 @@ +package org.asamk.signal.socket; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.InetAddress; +import java.nio.charset.Charset; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; +import net.sourceforge.argparse4j.inf.Namespace; + +import org.asamk.signal.manager.Manager; +import org.asamk.signal.socket.Commander; + +public class SocketTasks implements Runnable { + private String address; + private int port; + private Socket socket; + private OutputStream output = null; + private ObjectMapper mapper; + private Commander commander; + + public SocketTasks(Namespace namespace, Manager manager) { + this.address = namespace.getString("address"); + this.port = namespace.getInt("port"); + this.mapper = new ObjectMapper(); + this.commander = new Commander(manager, this); + } + + public void send(String message) { + if (message == null) return; + if (this.output == null || this.socket.isClosed()) { + System.err.println("WARNING: a message is ready but the socket isn't connected"); + return; + } + message += "\n"; + try { + this.output.write(message.getBytes(Charset.forName("UTF-8"))); + this.output.flush(); + } catch (SocketException e) { + System.err.println("Output socket connection lost!"); + this.output = null; + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void run() { + while (true) { + try { + ServerSocket serverSocket = new ServerSocket(this.port, 1, InetAddress.getByName(this.address)); + System.out.println("Socket ready, binding to address: " + this.address + ":" + this.port); + this.socket = serverSocket.accept(); // accept() blocks + this.output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input)); + System.out.println("Connection established."); + while (this.responseProcessor(bufferedReader.readLine())); // readLine() blocks + + bufferedReader.close(); + this.output.close(); + input.close(); + this.socket.close(); + serverSocket.close(); + } catch (Exception e) { System.err.println(e); } + System.err.println("Socket connection lost."); + try { + // Help relax potential resource hogging and log spam + TimeUnit.SECONDS.sleep(3); + } catch (Exception e) {} + } + } + + private boolean responseProcessor(String line) { + if (line == null) return false; + JsonNode node = null; + try { + node = this.mapper.readValue(line.trim(), JsonNode.class); + } catch (Exception e) { System.err.println(e); } + if (node == null) return true; + String reply = null; + Iterator> entryIt = node.fields(); + Map.Entry entry; + String command; + while (entryIt.hasNext()) { + entry = entryIt.next(); + command = entry.getKey(); + this.commander.handleCommand(command, entry.getValue()); + } + return true; + } + +} diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index d0638e41..b8c7e48a 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -152,6 +152,26 @@ public class SignalAccount implements Closeable { return !(!f.exists() || f.isDirectory()); } + public static String getSingleUser(String dataPath) { + File folder = new File(dataPath); + File[] listOfFiles = folder.listFiles(); + if (listOfFiles == null || listOfFiles.length <= 0) { + System.err.println("No user account found"); + return null; + } + String user = null; + for (int i = 0; i < listOfFiles.length; i++) { + if (listOfFiles[i].isFile()) { + if (user != null) { + System.err.println("Too many user accounts found"); + return null; // too many users + } + user = listOfFiles[i].getName(); + } + } + return user; + } + private void load() throws IOException { JsonNode rootNode; synchronized (fileChannel) { diff --git a/src/socketTest.py b/src/socketTest.py new file mode 100644 index 00000000..1b7e7b39 --- /dev/null +++ b/src/socketTest.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 +import socket +import time +import sys +import signal # optional: for SIGINT, SIGKILL +import json + +class SignalProcessor: + def __init__(self): + self.HOST = '127.0.0.1' + self.PORT = 24250 + self.messageQueue = [] + + def connect(self): + print(f'Connecting to {self.HOST}:{self.PORT}...') + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + self.sock.connect((self.HOST, self.PORT)) + self.sock.setblocking(False) # sock.settimeout(0.0) + break + except ConnectionRefusedError: + print('Refused, attemping to reconnect...') + time.sleep(3) + print('Connection established') + self.testCommands() + + def receive(self): + try: + buffer = self.sock.recv(65536) # should be plenty for large contact lists + except BlockingIOError: + return True + if not buffer: + print('Lost connection!') + return False + for line in filter(None, buffer.split(b'\n')): + self.messageQueue.append(line.decode()) + return True + + def send(self, message): + self.sock.sendall(message.encode() + b'\n'); + + def testCommands(self): + #self.send('{ "sendMessage" : { "contacts" : [ "+31638555555" ], "groups" : [ "Y5555rtl2p/TnLYvY555dA==", "DK555555UjPU55545557bA==" ], "message" : "This GROUP message comes from Python!" } }') + #self.send('{ "updateContacts" : { "+31638555555" : { "archived" : false } }, "getContacts" : "" }') + #self.send('{ "sendMessage" : { "contacts" : [ "+31638555555" ], "message" : "This PM comes from Python <3" } }') + #self.send('{ "getGroups" : "", "getContacts" : "" }') + self.send('{ "trust" : { "contacts" : [ "+31638555555" ] }, "endSession" : { "contacts" : [ "+31638555555" ] } }') + time.sleep(1) + + def handleMessages(self): + while len(signalCli.messageQueue): + rawMessage = signalCli.messageQueue.pop(0) + print(rawMessage) + obj = json.loads(rawMessage) + try: + source = obj['envelope']['source'] + msg = obj['envelope']['dataMessage']['message'] + except Exception: + continue + if msg.strip().lower() == 'love': + replyObj = { 'sendMessage': { 'contacts' : source.split(), 'message' : 'From Russia with ' + msg } } + self.send(json.dumps(replyObj)) + + def yourTimedCode(self): + print('yourStuff') + +def sigHandler(signal, frame): + print(f'Clean exit, received signal {signal}') + sys.exit(0) + +if __name__ == '__main__': + signal.signal(signal.SIGINT, sigHandler) + signal.signal(signal.SIGTERM, sigHandler) + signalCli = SignalProcessor() + signalCli.connect() + while True: + if signalCli.receive(): + signalCli.handleMessages() + signalCli.yourTimedCode() + time.sleep(1) + else: + signalCli.connect() + +# EOF