From 8aed35799458829ba9ac07c3bc25ff07a2369802 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sat, 31 Jul 2021 18:31:28 +0200 Subject: [PATCH] track attachment data (#671 and #316) create new DbusAttachment type allow URLs for --attachment option update manpage update wiki with signalmail implement setExpirationTimer() for DBus implement isRegistered() for DBus add sendNoteToSelfMessageWithDBusAttachments add sendGroupMessageWithDBusAttachments add sendMessageWithDBusAttachments bump version --- .gitignore | 3 + README.md | 2 +- build.gradle.kts | 2 +- .../org/asamk/signal/manager/Manager.java | 2 +- .../signal/manager/util/AttachmentUtils.java | 34 ++- .../org/asamk/signal/manager/util/Utils.java | 8 + signal-cli.1.gz | Bin 0 -> 5682 bytes src/main/java/org/asamk/Signal.java | 57 +++-- src/main/java/org/asamk/signal/App.java | 3 +- .../signal/JsonDbusReceiveMessageHandler.java | 38 ++- .../asamk/signal/ReceiveMessageHandler.java | 3 + .../asamk/signal/commands/DaemonCommand.java | 7 +- .../asamk/signal/commands/SendCommand.java | 29 ++- .../org/asamk/signal/dbus/DbusAttachment.java | 238 ++++++++++++++++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 94 ++++++- .../org/asamk/signal/json/JsonAttachment.java | 47 ++++ .../asamk/signal/json/JsonDataMessage.java | 2 + src/main/java/org/asamk/signal/util/Util.java | 6 + 18 files changed, 526 insertions(+), 49 deletions(-) create mode 100644 signal-cli.1.gz create mode 100644 src/main/java/org/asamk/signal/dbus/DbusAttachment.java diff --git a/.gitignore b/.gitignore index 8fa9c8bd..536bdd41 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ local.properties .settings/ out/ .DS_Store +.git/ +signal-cli +bin/ diff --git a/README.md b/README.md index f8eee725..67f2c397 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # signal-cli signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. -To be able to link to an existing Signal-Android/signal-cli instance, signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). +To be able to link to an existing Signal-Android/signal-cli instance, signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a secondary device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. signal-cli is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)), that can be used to send messages from any programming language that has dbus bindings. diff --git a/build.gradle.kts b/build.gradle.kts index c7de229c..da2a54bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { `check-lib-versions` } -version = "0.8.5" +version = "0.8.6" java { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index ef0b404b..183bfdea 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1274,7 +1274,7 @@ public class Manager implements Closeable { ) throws IOException, AttachmentInvalidException, InvalidNumberException { final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { - var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); + List attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); // Upload attachments here, so we only upload once even for multiple recipients var messageSender = createMessageSender(); diff --git a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java index aadadf95..e7caadab 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java @@ -11,6 +11,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.net.URL; public class AttachmentUtils { @@ -21,8 +22,13 @@ public class AttachmentUtils { for (var attachment : attachments) { try { signalServiceAttachments.add(createAttachment(new File(attachment))); - } catch (IOException e) { - throw new AttachmentInvalidException(attachment, e); + } catch (IOException f) { + // no such file, send it as URL + try { + signalServiceAttachments.add(createAttachment(new URL(attachment))); + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } } } } @@ -34,25 +40,39 @@ public class AttachmentUtils { return createAttachment(streamDetails, Optional.of(attachmentFile.getName())); } + public static SignalServiceAttachmentStream createAttachment(URL aURL) throws IOException { + final var streamDetails = Utils.createStreamDetailsFromURL(aURL); + String path = aURL.getPath(); + String name = path.substring(path.lastIndexOf('/') + 1); + return createAttachment(streamDetails, Optional.of(name)); + } + public static SignalServiceAttachmentStream createAttachment( StreamDetails streamDetails, Optional name ) { - // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option + // TODO maybe add a parameter to set the voiceNote, borderless, preview, width, height, caption, blurHash options final var uploadTimestamp = System.currentTimeMillis(); + boolean voicenote = false; + boolean borderless = false; Optional preview = Optional.absent(); + int width = 0; + int height = 0; Optional caption = Optional.absent(); Optional blurHash = Optional.absent(); final Optional resumableUploadSpec = Optional.absent(); + //ProgressListener listener = null; //Android OS + //CancellationSignal cancellationSignal = null; //Android OS; Signal developers misspelled class name + return new SignalServiceAttachmentStream(streamDetails.getStream(), streamDetails.getContentType(), streamDetails.getLength(), name, - false, - false, + voicenote, + borderless, false, preview, - 0, - 0, + width, + height, uploadTimestamp, caption, blurHash, diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index b0a9bbb1..1001a600 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -11,6 +11,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; @@ -36,6 +37,13 @@ public class Utils { return new StreamDetails(stream, mime, size); } + public static StreamDetails createStreamDetailsFromURL(URL aURL) throws IOException { + InputStream stream = aURL.openStream(); + final var mime = aURL.openConnection().getContentType(); + final var size = aURL.openConnection().getContentLengthLong(); + return new StreamDetails(stream, mime, size); + } + public static String computeSafetyNumber( boolean isUuidCapable, SignalServiceAddress ownAddress, diff --git a/signal-cli.1.gz b/signal-cli.1.gz new file mode 100644 index 0000000000000000000000000000000000000000..48c52d9f4c15f134ceade13dd974718bdb7af92d GIT binary patch literal 5682 zcmV-27R~7&iwFp|^bBDD19NF-ZeeUKV{BLWNSS2oYH?UXzgk>~AFdX|(Rex>oloE>y&gRKQNXjw<>Arj?5ux&>}GjS zy7d*j)$w3*bTOnM*lj%C`ef9_jh2ALVW?*oU7+uco~!OVc_U%Ro)~7rR;$!OBZl+X0$AD2nFiZnrTueWKl6DS~(aMrAt2hq>JRtBc4`m8ORd;Hh zXDR@~M7S)FY8^+oY8?lvQ|>N-kMlUnbd<-rft3!49?P(mY?j~NAt>Og1OI~$6((Y< zcQFyqwkE6iR?q_rmm~~MoB@=JfhZT>@M2^v(95kKgS2IILN`!ZUoNMWv)CxN@>7iYtwhDr_S-X zmy^K-8dielr~$f>Mslrc6C5oPHAf93diaI_4g@O&=m#7fR6hW1)C;ZBo|x9>u5*(W z%t1(`rIBRK0dj!)pw%D&)jidtN&aG!$utwU8W9#E_+h0d*(m8E_p5O8iE`qp@v~OFBU&?dGMN)m0#vflae4CLzY%6%ZQT4S`3i z*As2)uDWIeM_%8%-VPIlDu5V;Y;-vtUrxJcqvNU|#(9?HS#yTn=4ch8`Y%ukGGYNQ zAk9fasiUiIrtUI4I}F0ZcZNB+6L>mSooTg%!h^!Amo`rZ!LdSb%bJpEQj&{diPb4c z7cicg8t`1wbzmI-?1@nX+`Em_Yhd+)hcxk5tR>1$G0|5x*oDk9V5_1+RlAz(g z1;@rSBXwIq_#VvAtp+J0-;JYWj}Vvw{fiC5dY{J`3kiaw)w~fFJ~Z$*Axw%U!ENfE+|^^e%g5G?|=F0kF}Vo2$?8aA|f6Vu+l!# zx#tkh03VlWoF_)?L~-f5=!E3;*kkQlOE6!7^Cq2%k>so_v%Clc%qE(z(m2vTaX8>} z%^*2*gLVkR76=rMEEX7Kl_E@jh8gvch5J5iYV1F&H3&1%b+qrte^A5-?qBFyuhCYBU8VZ~^|O z<|B~O#IX)pw$eD2ECQGiuVEZ%Af`zq*eS)Z&%z6&f8 zkSFx5XTu<}&9Vz2hhZG8C7Tf`;49=O5QCC^oHdNwRTq3xoT{$OKyFq@M`HmV$I;*a z^0!O`ptfNQP`oJ!v55bQh`i3rrOGg~ntUd{g%cYq-sA4%ZV713SpRdi`k2Zp-n?LpWH!#|e8VGcx%c z`~b};yZdG`I%h-fB-Dx`rNa)Kv$K>S!AE#2H-_9?> z)slqLNs?T-P_V`+bSW(CsxbMbAgUp`&a8T}%Xx@D=;Rm81cIp{>GA`PW_(-QI6kJj zCpJnrgn$8s*oznbL{DuXQ5!3=)hJ-ggRn#~(4)|2ydKjaL@L1%V3S;+-9{2+9OD%@GpbCp(VJFY&3Y(~RWmeJ0= zUqmbtNL$s)@Q7(pC}~s;18};F4>&w6(>uRn@Bzg~S)*~*dteswF!B|ga;)j_Yyf#y zMba|}{^GngY^iY0S^%nU6d)B)N#j=PtU(5d)zuPM4HUeQA!LPsy^%ABzbt$R*KO4U zQGf0L-V;t;eUW&|`Tin0cE9WOPV@k_ym=F2Y(eS0p0i0SQ%jfw=ejh?RR-G10POZz zCSBF50M@G%%ZjoWZfcR7)>e3FL4Y`eB3mr6Sd<}7&El6#GRTvN$D=oc^OC~|-Hm4P zHAeMCO+ve9$<@EubGU+xTju6(3zeoZmV#o8TtxwJ86ycA(Vty!dU3kk^EcXCsz_nZ z&|78e&=pzc6x1U9TnQQLQE|vBiDEq$dDgQg)eA56~_sLEbMraLNLLVTBA7HtAQ6gHQ@zK zaB(H(EV~_)V)O@ICP3D*Ls>G4%nYXAm+d}~*S2J^zyO_4#P+a3_rusc9dDnrM_`X07i8eSoSw4?r;h0Oq*TMyMnt%>0ZJ0j_9~B!m7a`c9)> zJlxQDztktHLS5DiZL!oIph%uM;4IQb%8P8gUCRJ%fwFpWM^#F9J^5gvp^oqcmAkiI+wGw2aSTNCC zn1*UkHwlKKpQ_kk_%eb}s{v%GUM?xW;bDOd4RcJnon9|kydJOE9CSad*^sun?OctH zFR^g(L6HNx%THOJLrkWlv40qC*eoq{YO;Tx$uQ*>CX16sHKrZ=7AegzWFx|#3@6iz zKC-oYIy!pO9S^8oR({CZMMg@O7H~k#6TIbr7u^0!_<=4MRov&uhqtijm3zViWwqbh7&vx zrCC|z-I>xJqe#3WNDw8H^i4;vE_W%WLPQWSUjv#>Q8~tk)J8B<1!VBUIaZ!M0g|4H z!&&eo46nLWhJ+8bapecb1id_$D5>ZdTmMbhOMHV+l;^4{ewl3uBcI$I3**D-cY6^pX7#i7uI(-7)B;``x` zxTpHCS-Z5r9=;#G_i|XsrPR^C(iO3#{>NPs=TW^Bc5JJ4Np4oIv(T2b{d+WS(a zTU(QJ)l+P&qx7LHj}9LQaXX@PD+akiz^V;+tuiK%>nDZonB%nW^$9a_5oH>sI zUPZ;Iv^6vDkla%@PnrGW`mz{ai^jc<7^)+5C2v}-N85*f8JZw+0l6oRWJC^95tflE zGNLdx#%tyv_2V8%>@2h8;quCbIxUYN&f`?)CF`=SAW?gk!^G;d<5i*Vj-se4|4v;$ z|5gzapRi6&F6kkMn05pb!&HMdVY(Q#l+5YK;0f-q?EqwKZ3Cp1mel62nGaq#=g}s&2nR+_?M5mOI*h8y~1GZ5D0p71(?=pE&ucPJ3`CZ)IzV z>)ZacUk!ZZjf8x>3PEDdF4MdX0$!)~(uWcz#G?e>Dqe_5$=n3u@TZ(wN^C?6kfxzSeOT4zUesU>5f2e<2Lk*k~ z+Jj34(U)8ZMT;DeT6sUaXlQ^~3AO;r&<$Gy9yB^y}Gy~o#&(-^IEOGug65kE}hicwZ{0+;^Q*tZVcA_L+dN-E@=K z7L2xlbV~PWGzquBQh_Tlks~IqV1LCj_Tp39zsFgq_v#6yT)f0t$Wr1cd2M*+54mjY zDcPZ=yKRp8s=JVDjbi1Nzv`b2PrvUrq430ZAaZ6KKl+EG%V~Ey_-`2QER|5Oz z+kxC)HE#j)3l-0sVM6!S@YFm07+>YF{uV9FhvEe1ODx>xg(^R9k!oy5nPeK$?{qA= z1Y%z=DrB5HRtOKnGr{1)RGE3RJ1Ad9Q_YA=Y7ocj1(v9ZfUe8VL7<~JH9%-~xp~au zA$@YK3_FiN^BmP|uxcAm2-UKQgNp<`+scbg9IN@Bin_Vk!v>6bsLbBe7yJ89559i- z;`zaouBDLqmBrv~s@{`Dv_!h<3T$=ObB7{zERN4l&?WzmnvLCiAX~jzinIF>oB`F` zpWnv?JkiZNlXrTZubB)6vNKn`wv=$@9A=43Nmtz2{y=!8Z!&y^I*Umyx-Q;LEL_w}jZvbQY@Zmty<3lj-p2&EVqgxPSEK z?Q}Rj9sGW~gH!lEr#Yv8Iek64u#@K43j}k17JGZb_1Ow~+VQKZSy7oSWPxv>@t&8a zZ{cgY+ejDMlB^89i0M?PBZ}rd-ki3NGBBajO~R|(t5(GXQvBKwjQp99cRYmgA87i| zTl}{0BhmN>MD9lgkfHxWvti5CAUP(hltmB}hl&=a(Uy^VDT)9D*wl;#m6fPiQVF+EiSDC=8393m@ zLCNn1leF%Z-s;FaGl*|e4ZiE2jZX*mH7c*O&fPu~O?7@V+pfhD??lQbczpR|CwTwW z!IOh0&!0cX5By!+Y!pnnZ<}V*-luIOxr!BTt{ghR>eA_|4bnFgzuxyF{Yu(U3I8-fsFrfRVRRY47;^F2>nGa2*5VQb2^0>) z8Kj$xAaBKs>@Iu6n8qO14@e-@9&>`T7vRO2)b6c!I85Kj^&9b92_M&gSg!ASf%-kx z3u3Yv1~Xiq$bL0WfH5Y{(>Xg{x{Zue`{x%Z{HWafv0{{3VBaS|&)%VSYcI=P0$cJK Y9ALdiSb1cm`~R)~1$0x}FF8p70LSR$kpKVy literal 0 HcmV?d00001 diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index e7a21b88..22669412 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -4,6 +4,7 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.messages.DBusSignal; +import org.asamk.signal.dbus.DbusAttachment; import java.util.List; @@ -13,12 +14,21 @@ import java.util.List; */ public interface Signal extends DBusInterface { - long sendMessage( - String message, List attachments, String recipient + + long sendMessageWithDBusAttachments( + String message, List dBusAttachments, String recipient + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; + + long sendMessageWithDBusAttachments( + String message, List dBusAttachments, List recipients ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; long sendMessage( - String message, List attachments, List recipients + String message, List attachmentNames, String recipient + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; + + long sendMessage( + String message, List attachmentNames, List recipients ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; long sendRemoteDeleteMessage( @@ -42,13 +52,21 @@ public interface Signal extends DBusInterface { ) throws Error.InvalidNumber, Error.Failure; long sendNoteToSelfMessage( - String message, List attachments + String message, List attachmentNames + ) throws Error.AttachmentInvalid, Error.Failure; + + long sendNoteToSelfMessageWithDBusAttachments( + String message, List dBusAttachments ) throws Error.AttachmentInvalid, Error.Failure; void sendEndSessionMessage(List recipients) throws Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; long sendGroupMessage( - String message, List attachments, byte[] groupId + String message, List attachmentNames, byte[] groupId + ) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid; + + long sendGroupMessageWithDBusAttachments( + String message, List dBusAttachments, byte[] groupId ) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid; long sendGroupMessageReaction( @@ -59,6 +77,8 @@ public interface Signal extends DBusInterface { void setContactName(String number, String name) throws Error.InvalidNumber; + void setExpirationTimer(final String number, final int expiration) throws Error.InvalidNumber; + void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound; @@ -101,7 +121,7 @@ public interface Signal extends DBusInterface { private final String sender; private final byte[] groupId; private final String message; - private final List attachments; + private final List dBusAttachments; public MessageReceived( String objectpath, @@ -109,14 +129,14 @@ public interface Signal extends DBusInterface { String sender, byte[] groupId, String message, - List attachments + List dBusAttachments ) throws DBusException { - super(objectpath, timestamp, sender, groupId, message, attachments); + super(objectpath, timestamp, sender, groupId, message, dBusAttachments); this.timestamp = timestamp; this.sender = sender; this.groupId = groupId; this.message = message; - this.attachments = attachments; + this.dBusAttachments = dBusAttachments; } public long getTimestamp() { @@ -135,9 +155,10 @@ public interface Signal extends DBusInterface { return message; } - public List getAttachments() { - return attachments; - } + public List getAttachments() { + return dBusAttachments; + } + } class ReceiptReceived extends DBusSignal { @@ -167,7 +188,7 @@ public interface Signal extends DBusInterface { private final String destination; private final byte[] groupId; private final String message; - private final List attachments; + private final List dBusAttachments; public SyncMessageReceived( String objectpath, @@ -176,15 +197,15 @@ public interface Signal extends DBusInterface { String destination, byte[] groupId, String message, - List attachments + List dBusAttachments ) throws DBusException { - super(objectpath, timestamp, source, destination, groupId, message, attachments); + super(objectpath, timestamp, source, destination, groupId, message, dBusAttachments); this.timestamp = timestamp; this.source = source; this.destination = destination; this.groupId = groupId; this.message = message; - this.attachments = attachments; + this.dBusAttachments = dBusAttachments; } public long getTimestamp() { @@ -207,8 +228,8 @@ public interface Signal extends DBusInterface { return message; } - public List getAttachments() { - return attachments; + public List getAttachments() { + return dBusAttachments; } } diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 6cc73655..a7e69129 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -163,7 +163,7 @@ public class App { username = usernames.get(0); } else if (!PhoneNumberFormatter.isValidNumber(username, null)) { - throw new UserErrorException("Invalid username (phone number), make sure you include the country code."); + throw new UserErrorException("Invalid username (phone number), make sure you include a plus sign (+) followed by the country code."); } if (command instanceof RegistrationCommand) { @@ -264,6 +264,7 @@ public class App { try { manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); } catch (NotRegisteredException e) { + logger.debug("dataPath=" + dataPath + " serviceEnvironment=" + serviceEnvironment, e); throw new UserErrorException("User " + username + " is not registered."); } catch (Throwable e) { logger.debug("Loading state file failed", e); diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 5ed6af00..5f7eb940 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -1,6 +1,7 @@ package org.asamk.signal; import org.asamk.Signal; +import org.asamk.signal.dbus.DbusAttachment; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; @@ -9,6 +10,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import java.util.ArrayList; import java.util.List; @@ -71,12 +73,15 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { || message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.DELIVER )) { try { + List attachments = JsonDbusReceiveMessageHandler.getAttachments(message); + List dBusAttachments = JsonDbusReceiveMessageHandler.convertSignalAttachmentsToDbus(attachments); conn.sendMessage(new Signal.MessageReceived(objectPath, message.getTimestamp(), getLegacyIdentifier(sender), groupId != null ? groupId : new byte[0], message.getBody().isPresent() ? message.getBody().get() : "", - JsonDbusReceiveMessageHandler.getAttachments(message, m))); + dBusAttachments + )); } catch (DBusException e) { e.printStackTrace(); } @@ -93,6 +98,8 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { var groupId = getGroupId(message); try { + List attachments = JsonDbusReceiveMessageHandler.getAttachments(message); + List dBusAttachments = JsonDbusReceiveMessageHandler.convertSignalAttachmentsToDbus(attachments); conn.sendMessage(new Signal.SyncMessageReceived(objectPath, transcript.getTimestamp(), getLegacyIdentifier(sender), @@ -101,7 +108,8 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { : "", groupId != null ? groupId : new byte[0], message.getBody().isPresent() ? message.getBody().get() : "", - JsonDbusReceiveMessageHandler.getAttachments(message, m))); + dBusAttachments + )); } catch (DBusException e) { e.printStackTrace(); } @@ -116,7 +124,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { .serialize() : null; } - static private List getAttachments(SignalServiceDataMessage message, Manager m) { + static private List getAttachmentNames(SignalServiceDataMessage message, Manager m) { var attachments = new ArrayList(); if (message.getAttachments().isPresent()) { for (var attachment : message.getAttachments().get()) { @@ -128,10 +136,34 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { return attachments; } + + static private List getAttachments(SignalServiceDataMessage message) { + var attachments = new ArrayList(); + if (message.getAttachments().isPresent()) { + for (var attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + attachments.add(attachment); + } + } + } + return attachments; + } + @Override public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { super.handleMessage(envelope, content, exception); sendReceivedMessageToDbus(envelope, content, conn, objectPath, m); } + + static private List convertSignalAttachmentsToDbus(List attachments) { + ArrayList dBusAttachments = new ArrayList<>(); + if (!attachments.isEmpty()) { + for (SignalServiceAttachment attachment : attachments) { + DbusAttachment dBusAttachment = new DbusAttachment(attachment); + dBusAttachments.add(dBusAttachment); + } + } + return dBusAttachments; + } } diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 323b6edf..56496973 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -646,6 +646,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (pointer.getCaption().isPresent()) { writer.println("Caption: {}", pointer.getCaption().get()); } + if (pointer.getBlurHash().isPresent()) { + writer.println("Blur Hash: {}", pointer.getBlurHash().get()); + } if (pointer.getFileName().isPresent()) { writer.println("Filename: {}", pointer.getFileName().get()); } diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 7988c8ef..dfd15c24 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -65,7 +65,12 @@ public class DaemonCommand implements MultiLocalCommand { var objectPath = DbusConfig.getObjectPath(); var t = run(conn, objectPath, m, ignoreAttachments); - conn.requestBusName(DbusConfig.getBusname()); + try { + conn.requestBusName(DbusConfig.getBusname()); + } catch (DBusException e) { + logger.error("Dbus request command failed", e); + throw new UnexpectedErrorException("Dbus request command failed"); + } try { t.join(); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index af512def..26b2f543 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -11,6 +11,8 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.dbus.DbusAttachment; + import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; @@ -18,9 +20,11 @@ import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.List; public class SendCommand implements DbusCommand { @@ -87,13 +91,21 @@ public class SendCommand implements DbusCommand { } } - List attachments = ns.getList("attachment"); - if (attachments == null) { - attachments = List.of(); + List attachmentNames = ns.getList("attachment"); + if (attachmentNames == null) { + attachmentNames = List.of(); } final var writer = (PlainTextWriterImpl) outputWriter; + ArrayList dBusAttachments = new ArrayList<>(); + if (!attachmentNames.isEmpty()) { + for (var attachmentName : attachmentNames) { + DbusAttachment dBusAttachment = new DbusAttachment(attachmentName); + dBusAttachments.add(dBusAttachment); + } + } + if (groupIdString != null) { byte[] groupId; try { @@ -103,7 +115,7 @@ public class SendCommand implements DbusCommand { } try { - var timestamp = signal.sendGroupMessage(messageText, attachments, groupId); + var timestamp = signal.sendGroupMessage(messageText, attachmentNames, groupId); writer.println("{}", timestamp); return; } catch (DBusExecutionException e) { @@ -113,25 +125,26 @@ public class SendCommand implements DbusCommand { if (isNoteToSelf) { try { - var timestamp = signal.sendNoteToSelfMessage(messageText, attachments); + var timestamp = signal.sendNoteToSelfMessage(messageText, attachmentNames); writer.println("{}", timestamp); return; } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); + throw new UntrustedKeyErrorException("Failed to send note to self message: " + e.getMessage()); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage()); } } try { - var timestamp = signal.sendMessage(messageText, attachments, recipients); + var timestamp = signal.sendMessageWithDBusAttachments(messageText, dBusAttachments, recipients); writer.println("{}", timestamp); + } catch (UnknownObject e) { throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); } catch (Signal.Error.UntrustedIdentity e) { throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message, did not find attachment: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusAttachment.java b/src/main/java/org/asamk/signal/dbus/DbusAttachment.java new file mode 100644 index 00000000..b5060339 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusAttachment.java @@ -0,0 +1,238 @@ +package org.asamk.signal.dbus; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.util.StreamDetails; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.annotations.Position; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +import org.asamk.signal.manager.util.Utils; +import org.freedesktop.dbus.Struct; + +public final class DbusAttachment extends Struct +{ + @Position(0) + private String contentType; + @Position(1) + private String fileName; + @Position(2) + private String id; + @Position(3) + private Long size; + @Position(4) + private Integer keyLength; + @Position(5) + private boolean voiceNote; + @Position(6) + private Integer width; + @Position(7) + private Integer height; + @Position(8) + private String caption; + @Position(9) + private String blurHash; + +/* + * API = 2.15.3 from https://github.com/Turasa/libsignal-service-java (nonstandard) + public SignalServiceAttachmentStream(InputStream inputStream, + String contentType, + long length, + Optional fileName, + boolean voiceNote, + boolean borderless, + boolean gif, //nonstandard + Optional preview, + int width, + int height, + long uploadTimestamp, + Optional caption, + Optional blurHash, + ProgressListener listener, //Android OS + CancellationSignal cancellationSignal, //Android OS, Signal developers misspelled class name + Optional resumableUploadSpec) + + + public SignalServiceAttachmentPointer(int cdnNumber, + SignalServiceAttachmentRemoteId remoteId, + String contentType, + byte[] key, + Optional size, + Optional preview, + int width, + int height, + Optional digest, + Optional fileName, + boolean voiceNote, + boolean borderless, + Optional caption, + Optional blurHash, + long uploadTimestamp) + +other stuff : + private long id; // used by v2 attachments, see note + private int keyLength; //TODO: if you're going to do that, probably should have previewLength and digestLength + +notes : +"size" appears to be the same as "length" but is int rather than long +"length" represents file size (or stream/attachment size) +"preview" is also known as "thumbnail" + +from SignalServiceAttachmentRemoteId.java : + * Represents a signal service attachment identifier. This can be either a CDN key or a long, but + * not both at once. Attachments V2 used a long as an attachment identifier. This lacks sufficient + * entropy to reduce the likelihood of any two uploads going to the same location within a 30-day + * window. Attachments V3 uses an opaque string as an attachment identifier which provides more + * flexibility in the amount of entropy present. + + */ + + public DbusAttachment(SignalServiceAttachment attachment) { + this.contentType = attachment.getContentType(); + + if (attachment.isPointer()) { + final var pointer = attachment.asPointer(); + this.id = pointer.getRemoteId().toString(); + this.fileName = pointer.getFileName().orNull(); + if (this.fileName == null) { + this.fileName = ""; + } + this.size = pointer.getSize().transform(Integer::longValue).orNull(); + if (this.size == null) { + this.size = 0L; + } + this.setKeyLength(pointer.getKey().length); + this.setWidth(pointer.getWidth()); + this.setHeight(pointer.getHeight()); + this.setVoiceNote(pointer.getVoiceNote()); + if (pointer.getCaption().isPresent()) { + this.setCaption(pointer.getCaption().get()); + } else { + this.setCaption(""); + } + this.setBlurHash(""); + } else { + final var stream = attachment.asStream(); + this.fileName = stream.getFileName().orNull(); + if (this.fileName == null) { + this.fileName = ""; + } + this.id = ""; + this.size = stream.getLength(); + this.setKeyLength(0); + this.setWidth(0); + this.setHeight(0); + this.setVoiceNote(false); + this.setCaption(""); + this.setBlurHash(""); + } + } + + public DbusAttachment(String fileName) { + this.contentType = "application/octet-stream"; + try { + final File file = new File(fileName); + this.contentType = Utils.getFileMimeType(file, "application/octet-stream"); + this.size = file.length(); + } catch (IOException e) { + //no such file, try URL + try { + final URL aURL = new URL(fileName); + this.contentType = aURL.openConnection().getContentType(); + this.size = aURL.openConnection().getContentLengthLong(); + } catch (IOException f) { + f.printStackTrace(); + } + } + this.fileName = fileName; + this.id = ""; + this.setKeyLength(0); + this.setWidth(0); + this.setHeight(0); + this.setVoiceNote(false); + this.setCaption(""); + this.setBlurHash(""); + } + + public String getContentType() { + return contentType; + } + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + + public String getFileName() { + return fileName; + } + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Long getFileSize() { + return size; + } + public void setFileSize(Long size) { + this.size = size; + } + + public Integer getKeyLength() { + return keyLength; + } + public void setKeyLength(Integer keyLength) { + this.keyLength = keyLength; + } + + public Integer getWidth() { + return width; + } + public void setWidth(Integer width) { + this.width = width; + } + + public Integer getHeight() { + return height; + } + public void setHeight(Integer height) { + this.height = height; + } + + public boolean isVoiceNote() { + return voiceNote; + } + public boolean getVoiceNote() { + return voiceNote; + } + public void setVoiceNote(boolean voiceNote) { + this.voiceNote = voiceNote; + } + + public String getCaption() { + return caption; + } + public void setCaption(String caption) { + this.caption = caption; + } + + public String getBlurHash() { + return blurHash; + } + public void setBlurHash(String blurHash) { + this.blurHash = blurHash; + } + +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index b88aa81e..ae79c315 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -2,6 +2,8 @@ package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.BaseConfig; +import org.asamk.signal.PlainTextWriterImpl; +import org.asamk.signal.dbus.DbusAttachment; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -52,10 +54,36 @@ public class DbusSignalImpl implements Signal { } @Override - public long sendMessage(final String message, final List attachments, final String recipient) { + public long sendMessageWithDBusAttachments(final String message, final List dBusAttachments, final String recipient) { var recipients = new ArrayList(1); recipients.add(recipient); - return sendMessage(message, attachments, recipients); + return sendMessageWithDBusAttachments(message, dBusAttachments, recipients); + } + + @Override + public long sendMessageWithDBusAttachments(final String message, final List dBusAttachments, final List recipients) { + try { + ArrayList attachmentNames = new ArrayList<>(); + for (var dBusAttachment : dBusAttachments) { + attachmentNames.add(dBusAttachment.getFileName()); + } + final var results = m.sendMessage(message, attachmentNames, recipients); + checkSendMessageResults(results.first(), results.second()); + return results.first(); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + @Override + public long sendMessage(final String message, final List attachmentNames, final String recipient) { + var recipients = new ArrayList(1); + recipients.add(recipient); + return sendMessage(message, attachmentNames, recipients); } private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { @@ -98,9 +126,9 @@ public class DbusSignalImpl implements Signal { } @Override - public long sendMessage(final String message, final List attachments, final List recipients) { + public long sendMessage(final String message, final List attachmentNames, final List recipients) { try { - final var results = m.sendMessage(message, attachments, recipients); + final var results = m.sendMessage(message, attachmentNames, recipients); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (InvalidNumberException e) { @@ -185,10 +213,10 @@ public class DbusSignalImpl implements Signal { @Override public long sendNoteToSelfMessage( - final String message, final List attachments + final String message, final List attachmentNames ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { - final var results = m.sendSelfMessage(message, attachments); + final var results = m.sendSelfMessage(message, attachmentNames); checkSendMessageResult(results.first(), results.second()); return results.first(); } catch (AttachmentInvalidException e) { @@ -198,6 +226,25 @@ public class DbusSignalImpl implements Signal { } } + @Override + public long sendNoteToSelfMessageWithDBusAttachments( + final String message, final List dBusAttachments + ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { + try { + ArrayList attachmentNames = new ArrayList<>(); + for (var dBusAttachment : dBusAttachments) { + attachmentNames.add(dBusAttachment.getFileName()); + } + final var results = m.sendSelfMessage(message, attachmentNames); + checkSendMessageResult(results.first(), results.second()); + return results.first(); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + @Override public void sendEndSessionMessage(final List recipients) { try { @@ -211,9 +258,9 @@ public class DbusSignalImpl implements Signal { } @Override - public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { + public long sendGroupMessage(final String message, final List attachmentNames, final byte[] groupId) { try { - var results = m.sendGroupMessage(message, attachments, GroupId.unknownVersion(groupId)); + var results = m.sendGroupMessage(message, attachmentNames, GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { @@ -225,6 +272,26 @@ public class DbusSignalImpl implements Signal { } } + @Override + public long sendGroupMessageWithDBusAttachments(final String message, final List dBusAttachments, final byte[] groupId) { + try { + ArrayList attachmentNames = new ArrayList<>(); + for (var dBusAttachment : dBusAttachments) { + attachmentNames.add(dBusAttachment.getFileName()); + } + var results = m.sendGroupMessage(message, attachmentNames, GroupId.unknownVersion(groupId)); + checkSendMessageResults(results.first(), results.second()); + return results.first(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + + @Override public long sendGroupMessageReaction( final String emoji, @@ -272,6 +339,17 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void setExpirationTimer(final String number, final int expiration) { + try { + m.setExpirationTimer(number, expiration); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + @Override public void setContactBlocked(final String number, final boolean blocked) { try { diff --git a/src/main/java/org/asamk/signal/json/JsonAttachment.java b/src/main/java/org/asamk/signal/json/JsonAttachment.java index a96fc534..3fd2505d 100644 --- a/src/main/java/org/asamk/signal/json/JsonAttachment.java +++ b/src/main/java/org/asamk/signal/json/JsonAttachment.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.asamk.signal.dbus.DbusAttachment; + class JsonAttachment { @JsonProperty @@ -18,6 +20,33 @@ class JsonAttachment { @JsonProperty final Long size; + @JsonProperty + Integer keyLength; + + @JsonProperty + Integer width; + + @JsonProperty + Integer height; + + @JsonProperty + boolean voiceNote; + + @JsonProperty + String caption; + + @JsonProperty + String relay; + + @JsonProperty + byte[] preview; + + @JsonProperty + String digest; + + @JsonProperty + String blurHash; + JsonAttachment(SignalServiceAttachment attachment) { this.contentType = attachment.getContentType(); @@ -26,6 +55,11 @@ class JsonAttachment { this.id = pointer.getRemoteId().toString(); this.filename = pointer.getFileName().orNull(); this.size = pointer.getSize().transform(Integer::longValue).orNull(); + this.keyLength = pointer.getKey().length; + this.width = pointer.getWidth(); + this.height = pointer.getHeight(); + this.voiceNote = pointer.getVoiceNote(); + if (pointer.getCaption().isPresent()) {this.caption = pointer.getCaption().get();} } else { final var stream = attachment.asStream(); this.id = null; @@ -40,4 +74,17 @@ class JsonAttachment { this.id = null; this.size = null; } + + JsonAttachment(DbusAttachment attachment) { + this.contentType = attachment.getContentType(); + this.id = attachment.getId(); + this.filename = attachment.getFileName(); + this.size = attachment.getFileSize(); + this.keyLength = attachment.getKeyLength(); + this.width = attachment.getWidth(); + this.height = attachment.getHeight(); + this.voiceNote = attachment.getVoiceNote(); + this.caption = attachment.getCaption(); + } + } diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java index 6dbda978..f2c702f8 100644 --- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.Signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.dbus.DbusAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import java.util.List; @@ -126,6 +127,7 @@ class JsonDataMessage { sticker = null; contacts = null; attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList()); + } public JsonDataMessage(Signal.SyncMessageReceived messageReceived) { diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index a9d2bb8f..5001c4ae 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,8 +1,14 @@ package org.asamk.signal.util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.asamk.signal.dbus.DbusAttachment; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdFormatException; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.push.SignalServiceAddress; public class Util {