├── client ├── .gitignore ├── Cargo.toml └── src │ └── transports │ ├── tcp.rs │ ├── ipc.rs │ ├── mod.rs │ └── stream_codec.rs ├── data ├── signal-cli.sysusers.conf ├── org.asamk.Signal.service ├── signal-cli.tmpfiles.conf ├── signal-cli-socket.socket ├── signal-cli.service ├── signal-cli@.service ├── org.asamk.Signal.conf ├── signal-cli-socket.service └── org.asamk.SignalCli.svg ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── src └── main │ ├── resources │ └── META-INF │ │ └── services │ │ └── ch.qos.logback.classic.spi.Configurator │ └── java │ └── org │ └── asamk │ ├── signal │ ├── json │ │ ├── JsonRemoteDelete.java │ │ ├── JsonAttachmentData.java │ │ ├── JsonError.java │ │ ├── JsonPayment.java │ │ ├── JsonTextStyle.java │ │ ├── JsonContactAvatar.java │ │ ├── JsonPollTerminate.java │ │ ├── JsonContactEmail.java │ │ ├── JsonContactPhone.java │ │ ├── JsonEditMessage.java │ │ ├── JsonSticker.java │ │ ├── JsonPreview.java │ │ ├── JsonRecipientAddress.java │ │ ├── JsonStoryContext.java │ │ ├── JsonGroupInfo.java │ │ ├── JsonPollCreate.java │ │ ├── JsonMention.java │ │ ├── JsonTypingMessage.java │ │ ├── JsonContactName.java │ │ ├── JsonSyncReadMessage.java │ │ ├── JsonQuotedAttachment.java │ │ ├── JsonReceiptMessage.java │ │ ├── JsonSyncStoryMessage.java │ │ ├── JsonPollVote.java │ │ ├── JsonContactAddress.java │ │ ├── JsonReaction.java │ │ ├── JsonAttachment.java │ │ ├── JsonReceiveMessageHandler.java │ │ ├── JsonContact.java │ │ ├── JsonSyncDataMessage.java │ │ ├── JsonSharedContact.java │ │ └── JsonQuote.java │ ├── output │ │ ├── OutputWriter.java │ │ ├── JsonWriter.java │ │ ├── PlainTextWriter.java │ │ └── JsonWriterImpl.java │ ├── commands │ │ ├── CliCommand.java │ │ ├── SubparserAttacher.java │ │ ├── exceptions │ │ │ ├── UntrustedKeyErrorException.java │ │ │ ├── UnexpectedErrorException.java │ │ │ ├── IOErrorException.java │ │ │ ├── RateLimitErrorException.java │ │ │ ├── UserErrorException.java │ │ │ └── CommandException.java │ │ ├── Command.java │ │ ├── MessageRequestResponseType.java │ │ ├── JsonRpcSingleCommand.java │ │ ├── RegistrationCommand.java │ │ ├── JsonRpcMultiCommand.java │ │ ├── JsonRpcRegistrationCommand.java │ │ ├── LocalCommand.java │ │ ├── JsonRpcCommand.java │ │ ├── MultiLocalCommand.java │ │ ├── ProvisioningCommand.java │ │ ├── ReceiveMode.java │ │ ├── JsonRpcLocalCommand.java │ │ ├── JsonRpcMultiLocalCommand.java │ │ ├── SendContactsCommand.java │ │ ├── SendSyncRequestCommand.java │ │ ├── JsonRpcNamespace.java │ │ ├── RemovePinCommand.java │ │ ├── StartLinkCommand.java │ │ ├── ListAccountsCommand.java │ │ ├── UnregisterCommand.java │ │ ├── CommandHandler.java │ │ ├── RemoveDeviceCommand.java │ │ ├── SetPinCommand.java │ │ └── RemoveContactCommand.java │ ├── dbus │ │ ├── DbusUtils.java │ │ ├── DbusProperty.java │ │ ├── DbusInterfacePropertiesHandler.java │ │ └── DbusProvisioningManagerImpl.java │ ├── jsonrpc │ │ ├── JsonRpcMessage.java │ │ ├── JsonRpcException.java │ │ ├── JsonRpcBatchMessage.java │ │ └── JsonRpcSender.java │ ├── OutputType.java │ ├── logging │ │ └── ScrubberPatternLayout.java │ ├── TrustNewIdentityCli.java │ ├── ServiceEnvironmentCli.java │ ├── util │ │ ├── DateUtils.java │ │ ├── RandomUtils.java │ │ ├── Hex.java │ │ └── SecurityProvider.java │ ├── BaseConfig.java │ ├── DbusConfig.java │ └── http │ │ └── ServerSentEventSender.java │ └── SignalControl.java ├── FUNDING.yml ├── graalvm-config-dir ├── predefined-classes-config.json ├── serialization-config.json └── proxy-config.json ├── lib ├── src │ └── main │ │ ├── java │ │ └── org │ │ │ └── asamk │ │ │ └── signal │ │ │ └── manager │ │ │ ├── api │ │ │ ├── Pair.java │ │ │ ├── PinLockMissingException.java │ │ │ ├── ServiceEnvironment.java │ │ │ ├── GroupPermission.java │ │ │ ├── Device.java │ │ │ ├── TrustNewIdentity.java │ │ │ ├── GroupLinkState.java │ │ │ ├── ReceiveConfig.java │ │ │ ├── UserStatus.java │ │ │ ├── SendGroupMessageResults.java │ │ │ ├── UsernameStatus.java │ │ │ ├── NotRegisteredException.java │ │ │ ├── StickerPackInvalidException.java │ │ │ ├── GroupNotFoundException.java │ │ │ ├── NotPrimaryDeviceException.java │ │ │ ├── VerificationMethodNotAvailableException.java │ │ │ ├── Identity.java │ │ │ ├── GroupIdFormatException.java │ │ │ ├── LastGroupAdminException.java │ │ │ ├── NotAGroupMemberException.java │ │ │ ├── InvalidNumberException.java │ │ │ ├── AccountCheckException.java │ │ │ ├── GroupSendingNotAllowedException.java │ │ │ ├── AlreadyReceivingException.java │ │ │ ├── GroupIdV2.java │ │ │ ├── InvalidStickerException.java │ │ │ ├── InvalidUsernameException.java │ │ │ ├── PinLockedException.java │ │ │ ├── AttachmentInvalidException.java │ │ │ ├── InactiveGroupLinkException.java │ │ │ ├── InvalidDeviceLinkException.java │ │ │ ├── IncorrectPinException.java │ │ │ ├── DeviceLimitExceededException.java │ │ │ ├── PendingAdminApprovalException.java │ │ │ ├── NonNormalizedPhoneNumberException.java │ │ │ ├── RateLimitException.java │ │ │ ├── CaptchaRejectedException.java │ │ │ ├── UnregisteredRecipientException.java │ │ │ ├── PhoneNumberSharingMode.java │ │ │ ├── TypingAction.java │ │ │ ├── Color.java │ │ │ ├── GroupIdV1.java │ │ │ ├── UserAlreadyExistsException.java │ │ │ ├── UntrustedIdentityException.java │ │ │ ├── StickerPack.java │ │ │ ├── CaptchaRequiredException.java │ │ │ ├── Configuration.java │ │ │ ├── StickerPackId.java │ │ │ ├── IdentityVerificationCode.java │ │ │ ├── SendMessageResults.java │ │ │ ├── Message.java │ │ │ ├── ProofRequiredException.java │ │ │ ├── TrustLevel.java │ │ │ ├── GroupId.java │ │ │ └── SendMessageResult.java │ │ │ ├── config │ │ │ ├── KeyBackupConfig.java │ │ │ ├── WhisperTrustStore.java │ │ │ └── ServiceEnvironmentConfig.java │ │ │ ├── storage │ │ │ ├── recipients │ │ │ │ ├── RecipientWithAddress.java │ │ │ │ ├── RecipientIdCreator.java │ │ │ │ ├── SelfAddressProvider.java │ │ │ │ ├── InvalidAddress.java │ │ │ │ ├── SelfProfileKeyProvider.java │ │ │ │ ├── RecipientId.java │ │ │ │ ├── LegacyRecipientStore.java │ │ │ │ └── RecipientResolver.java │ │ │ ├── keyValue │ │ │ │ └── KeyValueEntry.java │ │ │ ├── accounts │ │ │ │ └── AccountsStorage.java │ │ │ ├── threads │ │ │ │ └── LegacyThreadInfo.java │ │ │ ├── profiles │ │ │ │ ├── LegacySignalProfileEntry.java │ │ │ │ └── ProfileStore.java │ │ │ ├── stickers │ │ │ │ ├── StickerPack.java │ │ │ │ └── LegacyStickerStore.java │ │ │ ├── sendLog │ │ │ │ └── MessageSendLogEntry.java │ │ │ ├── contacts │ │ │ │ ├── LegacyJsonContactsStore.java │ │ │ │ ├── ContactsStore.java │ │ │ │ └── LegacyContactInfo.java │ │ │ ├── stickerPacks │ │ │ │ └── JsonStickerPack.java │ │ │ ├── protocol │ │ │ │ ├── LegacySessionInfo.java │ │ │ │ ├── LegacyIdentityInfo.java │ │ │ │ ├── LegacyJsonSignalProtocolStore.java │ │ │ │ ├── LegacyJsonPreKeyStore.java │ │ │ │ └── LegacyJsonSignedPreKeyStore.java │ │ │ ├── identities │ │ │ │ ├── IdentityInfo.java │ │ │ │ └── SignalIdentityKeyStore.java │ │ │ ├── configuration │ │ │ │ └── LegacyConfigurationStore.java │ │ │ └── messageCache │ │ │ │ └── CachedMessage.java │ │ │ ├── jobs │ │ │ ├── Job.java │ │ │ ├── CleanOldPreKeysJob.java │ │ │ ├── CheckWhoAmIJob.java │ │ │ ├── RefreshRecipientsJob.java │ │ │ ├── DownloadProfileAvatarJob.java │ │ │ ├── DownloadProfileJob.java │ │ │ ├── SyncStorageJob.java │ │ │ └── RetrieveStickerPackJob.java │ │ │ ├── ManagerLogger.java │ │ │ ├── helper │ │ │ ├── AccountFileUpdater.java │ │ │ └── RecipientAddressResolver.java │ │ │ ├── actions │ │ │ ├── HandleAction.java │ │ │ ├── SendSyncKeysAction.java │ │ │ ├── SendSyncGroupsAction.java │ │ │ ├── RefreshPreKeysAction.java │ │ │ ├── SendSyncContactsAction.java │ │ │ ├── SendSyncBlockedListAction.java │ │ │ ├── SendSyncConfigurationAction.java │ │ │ ├── UpdateAccountAttributesAction.java │ │ │ ├── SyncStorageDataAction.java │ │ │ ├── RetrieveProfileAction.java │ │ │ ├── SendProfileKeyAction.java │ │ │ ├── SendGroupInfoAction.java │ │ │ ├── SendGroupInfoRequestAction.java │ │ │ ├── RenewSessionAction.java │ │ │ ├── ResendMessageAction.java │ │ │ └── SendReceiptAction.java │ │ │ ├── Settings.java │ │ │ ├── syncStorage │ │ │ ├── StorageRecordUpdate.java │ │ │ ├── StorageRecordProcessor.java │ │ │ └── WriteOperationResult.java │ │ │ ├── ProvisioningManager.java │ │ │ ├── internal │ │ │ ├── ReentrantSignalSessionLock.java │ │ │ ├── PathConfig.java │ │ │ ├── AccountFileUpdaterImpl.java │ │ │ └── LibSignalLogger.java │ │ │ ├── MultiAccountManager.java │ │ │ ├── groups │ │ │ └── GroupLinkPassword.java │ │ │ ├── RegistrationManager.java │ │ │ └── util │ │ │ ├── MimeUtils.java │ │ │ └── AttachmentUtils.java │ │ └── resources │ │ └── org │ │ └── asamk │ │ └── signal │ │ └── manager │ │ └── config │ │ └── whisper.store └── build.gradle.kts ├── .idea └── codeStyles │ └── codeStyleConfig.xml ├── .gitignore ├── settings.gradle.kts ├── man └── Makefile ├── Containerfile ├── native.Containerfile ├── CONTRIBUTING.md └── .github └── workflows └── codeql-analysis.yml /client/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /data/signal-cli.sysusers.conf: -------------------------------------------------------------------------------- 1 | u signal-cli - "Signal messaging service" /var/lib/signal-cli 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsamK/signal-cli/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator: -------------------------------------------------------------------------------- 1 | org.asamk.signal.logging.LogConfigurator 2 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: AsamK 2 | liberapay: asamk 3 | ko_fi: asamk 4 | #bitcoin: bc1qykae53fry8a8ycgdzgv0rlxfc959hmmllvz698 5 | -------------------------------------------------------------------------------- /data/org.asamk.Signal.service: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.asamk.Signal 3 | Exec=/bin/false 4 | SystemdService=dbus-org.asamk.Signal.service 5 | -------------------------------------------------------------------------------- /graalvm-config-dir/predefined-classes-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type":"agent-extracted", 4 | "classes":[ 5 | ] 6 | } 7 | ] 8 | 9 | -------------------------------------------------------------------------------- /graalvm-config-dir/serialization-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types":[ 3 | ], 4 | "lambdaCapturingTypes":[ 5 | ], 6 | "proxies":[ 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonRemoteDelete.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | record JsonRemoteDelete(long timestamp) {} 4 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/Pair.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public record Pair(T first, U second) {} 4 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonAttachmentData.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | public record JsonAttachmentData( 4 | String data 5 | ) {} 6 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/output/OutputWriter.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.output; 2 | 3 | public sealed interface OutputWriter permits JsonWriter, PlainTextWriter {} 4 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/PinLockMissingException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class PinLockMissingException extends Exception {} 4 | -------------------------------------------------------------------------------- /lib/src/main/resources/org/asamk/signal/manager/config/whisper.store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsamK/signal-cli/HEAD/lib/src/main/resources/org/asamk/signal/manager/config/whisper.store -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/ServiceEnvironment.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public enum ServiceEnvironment { 4 | LIVE, 5 | STAGING, 6 | } 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/GroupPermission.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public enum GroupPermission { 4 | EVERY_MEMBER, 5 | ONLY_ADMINS, 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/Device.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public record Device(int id, String name, long created, long lastSeen, boolean isThisDevice) {} 4 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/TrustNewIdentity.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public enum TrustNewIdentity { 4 | ALWAYS, 5 | ON_FIRST_USE, 6 | NEVER 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/config/KeyBackupConfig.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.config; 2 | 3 | public record KeyBackupConfig(String enclaveName, byte[] serviceId, String mrenclave) {} 4 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/output/JsonWriter.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.output; 2 | 3 | public non-sealed interface JsonWriter extends OutputWriter { 4 | 5 | void write(final Object object); 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/GroupLinkState.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public enum GroupLinkState { 4 | ENABLED, 5 | ENABLED_WITH_APPROVAL, 6 | DISABLED, 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/ReceiveConfig.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean sendReadReceipts) {} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/* 3 | !.idea/codeStyles/ 4 | build/ 5 | *~ 6 | *.swp 7 | *.iml 8 | local.properties 9 | .classpath 10 | .project 11 | .settings/ 12 | out/ 13 | .DS_Store 14 | /bin/ 15 | /test-config/ 16 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientWithAddress.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.recipients; 2 | 3 | public record RecipientWithAddress(RecipientId id, RecipientAddress address) {} 4 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/jobs/Job.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.jobs; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public interface Job { 6 | 7 | void run(Context context); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/UserStatus.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.UUID; 4 | 5 | public record UserStatus(String number, UUID uuid, boolean unrestrictedUnidentifiedAccess) {} 6 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.List; 4 | 5 | public record SendGroupMessageResults(long timestamp, List results) {} 6 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientIdCreator.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.recipients; 2 | 3 | public interface RecipientIdCreator { 4 | 5 | RecipientId create(long recipientId); 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/recipients/SelfAddressProvider.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.recipients; 2 | 3 | public interface SelfAddressProvider { 4 | 5 | RecipientAddress getSelfAddress(); 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/UsernameStatus.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.UUID; 4 | 5 | public record UsernameStatus(String username, UUID uuid, boolean unrestrictedUnidentifiedAccess) {} 6 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/CliCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Subparser; 4 | 5 | public interface CliCommand extends Command { 6 | 7 | void attachToSubparser(Subparser subparser); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/SubparserAttacher.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Subparser; 4 | 5 | public interface SubparserAttacher { 6 | 7 | void attachToSubparser(final Subparser subparser); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/NotRegisteredException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class NotRegisteredException extends Exception { 4 | 5 | public NotRegisteredException() { 6 | super("User is not registered."); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/recipients/InvalidAddress.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.recipients; 2 | 3 | public class InvalidAddress extends AssertionError { 4 | 5 | InvalidAddress(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | mavenLocal() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.name = "signal-cli" 9 | 10 | include("libsignal-cli") 11 | project(":libsignal-cli").projectDir = file("lib") 12 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/StickerPackInvalidException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class StickerPackInvalidException extends Exception { 4 | 5 | public StickerPackInvalidException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/recipients/SelfProfileKeyProvider.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.recipients; 2 | 3 | import org.signal.libsignal.zkgroup.profiles.ProfileKey; 4 | 5 | public interface SelfProfileKeyProvider { 6 | 7 | ProfileKey getSelfProfileKey(); 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/ManagerLogger.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager; 2 | 3 | import org.asamk.signal.manager.internal.LibSignalLogger; 4 | 5 | public class ManagerLogger { 6 | 7 | public static void initLogger() { 8 | LibSignalLogger.initLogger(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/GroupNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class GroupNotFoundException extends Exception { 4 | 5 | public GroupNotFoundException(GroupId groupId) { 6 | super("Group not found: " + groupId.toBase64()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/NotPrimaryDeviceException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class NotPrimaryDeviceException extends Exception { 4 | 5 | public NotPrimaryDeviceException() { 6 | super("This function is not supported for linked devices."); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonError.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | public record JsonError(String message, String type) { 4 | 5 | public static JsonError from(Throwable exception) { 6 | return new JsonError(exception.getMessage(), exception.getClass().getSimpleName()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/exceptions/UntrustedKeyErrorException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands.exceptions; 2 | 3 | public final class UntrustedKeyErrorException extends CommandException { 4 | 5 | public UntrustedKeyErrorException(final String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueEntry.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.keyValue; 2 | 3 | public record KeyValueEntry(String key, Class clazz, T defaultValue) { 4 | 5 | public KeyValueEntry(String key, Class clazz) { 6 | this(key, clazz, null); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/helper/AccountFileUpdater.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.helper; 2 | 3 | import org.signal.core.models.ServiceId.ACI; 4 | 5 | public interface AccountFileUpdater { 6 | 7 | void updateAccountIdentifiers(String number, ACI aci); 8 | 9 | void removeAccount(); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/VerificationMethodNotAvailableException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class VerificationMethodNotAvailableException extends Exception { 4 | 5 | public VerificationMethodNotAvailableException() { 6 | super("Invalid verification method"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/exceptions/UnexpectedErrorException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands.exceptions; 2 | 3 | public final class UnexpectedErrorException extends CommandException { 4 | 5 | public UnexpectedErrorException(final String message, final Throwable cause) { 6 | super(message, cause); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/dbus/DbusUtils.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.dbus; 2 | 3 | public final class DbusUtils { 4 | 5 | private DbusUtils() { 6 | 7 | } 8 | 9 | public static String makeValidObjectPathElement(String pathElement) { 10 | return pathElement.replaceAll("[^A-Za-z0-9_]", "_"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/accounts/AccountsStorage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.accounts; 2 | 3 | import java.util.List; 4 | 5 | public record AccountsStorage(List accounts, Integer version) { 6 | 7 | public record Account(String path, String environment, String number, String uuid) {} 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public interface HandleAction { 6 | 7 | void execute(Context context) throws Throwable; 8 | 9 | default void mergeOther(HandleAction action) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /data/signal-cli.tmpfiles.conf: -------------------------------------------------------------------------------- 1 | d /var/lib/signal-cli 0755 signal-cli signal-cli - 2 | d /var/lib/signal-cli/data 0700 signal-cli signal-cli - 3 | d /var/lib/signal-cli/attachments 0750 signal-cli signal-cli - 4 | d /var/lib/signal-cli/avatars 0750 signal-cli signal-cli - 5 | d /var/lib/signal-cli/stickers 0750 signal-cli signal-cli - 6 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/Settings.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager; 2 | 3 | import org.asamk.signal.manager.api.TrustNewIdentity; 4 | 5 | public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) { 6 | 7 | public static final Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/Identity.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public record Identity( 4 | RecipientAddress recipient, 5 | byte[] fingerprint, 6 | String safetyNumber, 7 | byte[] scannableSafetyNumber, 8 | TrustLevel trustLevel, 9 | long dateAddedTimestamp 10 | ) {} 11 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/exceptions/IOErrorException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands.exceptions; 2 | 3 | import java.io.IOException; 4 | 5 | public final class IOErrorException extends CommandException { 6 | 7 | public IOErrorException(final String message, IOException cause) { 8 | super(message, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/GroupIdFormatException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class GroupIdFormatException extends Exception { 4 | 5 | public GroupIdFormatException(String groupId, Throwable e) { 6 | super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage(), e); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/LastGroupAdminException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class LastGroupAdminException extends Exception { 4 | 5 | public LastGroupAdminException(GroupId groupId, String groupName) { 6 | super("User is last admin in group: " + groupName + " (" + groupId.toBase64() + ")"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/NotAGroupMemberException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class NotAGroupMemberException extends Exception { 4 | 5 | public NotAGroupMemberException(GroupId groupId, String groupName) { 6 | super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/threads/LegacyThreadInfo.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.threads; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class LegacyThreadInfo { 6 | 7 | @JsonProperty 8 | public String id; 9 | 10 | @JsonProperty 11 | public int messageExpirationTime; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/Command.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import org.asamk.signal.OutputType; 4 | 5 | import java.util.List; 6 | 7 | public interface Command { 8 | 9 | String getName(); 10 | 11 | default List getSupportedOutputTypes() { 12 | return List.of(OutputType.PLAIN_TEXT); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonPayment.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | public record JsonPayment(String note, byte[] receipt) { 6 | 7 | static JsonPayment from(MessageEnvelope.Data.Payment payment) { 8 | return new JsonPayment(payment.note(), payment.receipt()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/jsonrpc/JsonRpcMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.jsonrpc; 2 | 3 | /** 4 | * Represents a JSON-RPC (batch) request or (batch) response. 5 | * https://www.jsonrpc.org/specification 6 | */ 7 | public sealed abstract class JsonRpcMessage permits JsonRpcBatchMessage, JsonRpcRequest, JsonRpcResponse {} 8 | -------------------------------------------------------------------------------- /data/signal-cli-socket.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Send secure messages to Signal clients 3 | 4 | [Socket] 5 | ListenStream=%t/signal-cli/socket 6 | SocketUser=root 7 | # Add yourself to the signal-cli group to talk with the service 8 | # Run 'usermod -aG signal-cli yourusername' 9 | SocketGroup=signal-cli 10 | SocketMode=0660 11 | 12 | [Install] 13 | WantedBy=sockets.target 14 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/InvalidNumberException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class InvalidNumberException extends Exception { 4 | 5 | public InvalidNumberException(String message) { 6 | super(message); 7 | } 8 | 9 | InvalidNumberException(String message, Throwable e) { 10 | super(message, e); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/AccountCheckException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class AccountCheckException extends Exception { 4 | 5 | public AccountCheckException(String message) { 6 | super(message); 7 | } 8 | 9 | public AccountCheckException(String message, Exception e) { 10 | super(message, e); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/OutputType.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal; 2 | 3 | public enum OutputType { 4 | PLAIN_TEXT { 5 | @Override 6 | public String toString() { 7 | return "plain-text"; 8 | } 9 | }, 10 | JSON { 11 | @Override 12 | public String toString() { 13 | return "json"; 14 | } 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonTextStyle.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.TextStyle; 4 | 5 | public record JsonTextStyle(String style, int start, int length) { 6 | 7 | static JsonTextStyle from(TextStyle textStyle) { 8 | return new JsonTextStyle(textStyle.style().name(), textStyle.start(), textStyle.length()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/GroupSendingNotAllowedException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class GroupSendingNotAllowedException extends Exception { 4 | 5 | public GroupSendingNotAllowedException(GroupId groupId, String groupName) { 6 | super("User is not allowed to send message to group: " + groupName + " (" + groupId.toBase64() + ")"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.helper; 2 | 3 | import org.asamk.signal.manager.storage.recipients.RecipientAddress; 4 | import org.asamk.signal.manager.storage.recipients.RecipientId; 5 | 6 | public interface RecipientAddressResolver { 7 | 8 | RecipientAddress resolveRecipientAddress(RecipientId recipientId); 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/AlreadyReceivingException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class AlreadyReceivingException extends Exception { 4 | 5 | public AlreadyReceivingException(String message) { 6 | super(message); 7 | } 8 | 9 | public AlreadyReceivingException(String message, Exception e) { 10 | super(message, e); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/exceptions/RateLimitErrorException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands.exceptions; 2 | 3 | import org.asamk.signal.manager.api.RateLimitException; 4 | 5 | public final class RateLimitErrorException extends CommandException { 6 | 7 | public RateLimitErrorException(final String message, final RateLimitException cause) { 8 | super(message, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/GroupIdV2.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.Base64; 4 | 5 | public final class GroupIdV2 extends GroupId { 6 | 7 | public static GroupIdV2 fromBase64(String groupId) { 8 | return new GroupIdV2(Base64.getDecoder().decode(groupId)); 9 | } 10 | 11 | public GroupIdV2(final byte[] id) { 12 | super(id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/jsonrpc/JsonRpcException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.jsonrpc; 2 | 3 | public class JsonRpcException extends Exception { 4 | 5 | private final JsonRpcResponse.Error error; 6 | 7 | public JsonRpcException(final JsonRpcResponse.Error error) { 8 | this.error = error; 9 | } 10 | 11 | public JsonRpcResponse.Error getError() { 12 | return error; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/InvalidStickerException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class InvalidStickerException extends Exception { 4 | 5 | public InvalidStickerException(final String message) { 6 | super(message); 7 | } 8 | 9 | public InvalidStickerException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/InvalidUsernameException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class InvalidUsernameException extends Exception { 4 | 5 | public InvalidUsernameException(final String message) { 6 | super(message); 7 | } 8 | 9 | public InvalidUsernameException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/MessageRequestResponseType.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | enum MessageRequestResponseType { 4 | ACCEPT { 5 | @Override 6 | public String toString() { 7 | return "accept"; 8 | } 9 | }, 10 | DELETE { 11 | @Override 12 | public String toString() { 13 | return "delete"; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/PinLockedException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class PinLockedException extends Exception { 4 | 5 | private final long timeRemaining; 6 | 7 | public PinLockedException(long timeRemaining) { 8 | this.timeRemaining = timeRemaining; 9 | } 10 | 11 | public long getTimeRemaining() { 12 | return timeRemaining; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands.exceptions; 2 | 3 | public final class UserErrorException extends CommandException { 4 | 5 | public UserErrorException(final String message) { 6 | super(message); 7 | } 8 | 9 | public UserErrorException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/logging/ScrubberPatternLayout.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.logging; 2 | 3 | import ch.qos.logback.classic.PatternLayout; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | 6 | public class ScrubberPatternLayout extends PatternLayout { 7 | 8 | @Override 9 | public String doLayout(ILoggingEvent event) { 10 | return Scrubber.scrub(super.doLayout(event)).toString(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/AttachmentInvalidException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class AttachmentInvalidException extends Exception { 4 | 5 | public AttachmentInvalidException(String message) { 6 | super(message); 7 | } 8 | 9 | public AttachmentInvalidException(String attachment, Exception e) { 10 | super(attachment + ": " + e.getMessage()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/InactiveGroupLinkException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class InactiveGroupLinkException extends Exception { 4 | 5 | public InactiveGroupLinkException(final String message) { 6 | super(message); 7 | } 8 | 9 | public InactiveGroupLinkException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/InvalidDeviceLinkException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class InvalidDeviceLinkException extends Exception { 4 | 5 | public InvalidDeviceLinkException(final String message) { 6 | super(message); 7 | } 8 | 9 | public InvalidDeviceLinkException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.profiles; 2 | 3 | import org.asamk.signal.manager.storage.recipients.RecipientAddress; 4 | import org.signal.libsignal.zkgroup.profiles.ProfileKey; 5 | 6 | public record LegacySignalProfileEntry( 7 | RecipientAddress address, ProfileKey profileKey, long lastUpdateTimestamp, LegacySignalProfile profile 8 | ) {} 9 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/IncorrectPinException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class IncorrectPinException extends Exception { 4 | 5 | private final int triesRemaining; 6 | 7 | public IncorrectPinException(int triesRemaining) { 8 | this.triesRemaining = triesRemaining; 9 | } 10 | 11 | public int getTriesRemaining() { 12 | return triesRemaining; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/stickers/StickerPack.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.stickers; 2 | 3 | import org.asamk.signal.manager.api.StickerPackId; 4 | 5 | public record StickerPack(long internalId, StickerPackId packId, byte[] packKey, boolean isInstalled) { 6 | 7 | public StickerPack(final StickerPackId packId, final byte[] packKey) { 8 | this(-1, packId, packKey, false); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/DeviceLimitExceededException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class DeviceLimitExceededException extends Exception { 4 | 5 | public DeviceLimitExceededException(final String message) { 6 | super(message); 7 | } 8 | 9 | public DeviceLimitExceededException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/PendingAdminApprovalException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class PendingAdminApprovalException extends Exception { 4 | 5 | public PendingAdminApprovalException(final String message) { 6 | super(message); 7 | } 8 | 9 | public PendingAdminApprovalException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/JsonRpcSingleCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import org.asamk.signal.commands.exceptions.CommandException; 4 | import org.asamk.signal.manager.Manager; 5 | import org.asamk.signal.output.JsonWriter; 6 | 7 | public interface JsonRpcSingleCommand extends JsonRpcCommand { 8 | 9 | void handleCommand(T request, Manager m, JsonWriter jsonWriter) throws CommandException; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/RegistrationCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Namespace; 4 | 5 | import org.asamk.signal.commands.exceptions.CommandException; 6 | import org.asamk.signal.manager.RegistrationManager; 7 | 8 | public interface RegistrationCommand extends CliCommand { 9 | 10 | void handleCommand(Namespace ns, RegistrationManager m) throws CommandException; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonContactAvatar.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | public record JsonContactAvatar(JsonAttachment attachment, boolean isProfile) { 6 | 7 | static JsonContactAvatar from(MessageEnvelope.Data.SharedContact.Avatar avatar) { 8 | return new JsonContactAvatar(JsonAttachment.from(avatar.attachment()), avatar.isProfile()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /data/signal-cli.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Send secure messages to Signal clients 3 | Requires=dbus.socket 4 | After=dbus.socket 5 | Wants=network-online.target 6 | After=network-online.target 7 | 8 | [Service] 9 | Type=dbus 10 | Environment="SIGNAL_CLI_OPTS=-Xms2m" 11 | ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --dbus-system 12 | User=signal-cli 13 | BusName=org.asamk.Signal 14 | 15 | [Install] 16 | Alias=dbus-org.asamk.Signal.service 17 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/NonNormalizedPhoneNumberException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class NonNormalizedPhoneNumberException extends Exception { 4 | 5 | public NonNormalizedPhoneNumberException(final String message) { 6 | super(message); 7 | } 8 | 9 | public NonNormalizedPhoneNumberException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/JsonRpcMultiCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import org.asamk.signal.commands.exceptions.CommandException; 4 | import org.asamk.signal.manager.MultiAccountManager; 5 | import org.asamk.signal.output.JsonWriter; 6 | 7 | public interface JsonRpcMultiCommand extends JsonRpcCommand { 8 | 9 | void handleCommand(T request, MultiAccountManager c, JsonWriter jsonWriter) throws CommandException; 10 | } 11 | -------------------------------------------------------------------------------- /data/signal-cli@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Send secure messages to Signal clients 3 | Requires=dbus.socket 4 | After=dbus.socket 5 | Wants=network-online.target 6 | After=network-online.target 7 | 8 | [Service] 9 | Type=dbus 10 | Environment="SIGNAL_CLI_OPTS=-Xms2m" 11 | ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --dbus-system 12 | User=signal-cli 13 | BusName=org.asamk.Signal 14 | 15 | [Install] 16 | Alias=dbus-org.asamk.Signal.service 17 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonPollTerminate.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | public record JsonPollTerminate(long targetSentTimestamp) { 6 | 7 | static JsonPollTerminate from(MessageEnvelope.Data.PollTerminate pollTerminate) { 8 | final var targetSentTimestamp = pollTerminate.targetSentTimestamp(); 9 | 10 | return new JsonPollTerminate(targetSentTimestamp); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/JsonRpcRegistrationCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import org.asamk.signal.commands.exceptions.CommandException; 4 | import org.asamk.signal.manager.RegistrationManager; 5 | import org.asamk.signal.output.JsonWriter; 6 | 7 | public interface JsonRpcRegistrationCommand extends JsonRpcCommand { 8 | 9 | void handleCommand(T request, RegistrationManager m, JsonWriter jsonWriter) throws CommandException; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/LocalCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Namespace; 4 | 5 | import org.asamk.signal.commands.exceptions.CommandException; 6 | import org.asamk.signal.manager.Manager; 7 | import org.asamk.signal.output.OutputWriter; 8 | 9 | public interface LocalCommand extends CliCommand { 10 | 11 | void handleCommand(Namespace ns, Manager m, OutputWriter outputWriter) throws CommandException; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonContactEmail.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | import org.asamk.signal.util.Util; 5 | 6 | public record JsonContactEmail(String value, String type, String label) { 7 | 8 | static JsonContactEmail from(MessageEnvelope.Data.SharedContact.Email email) { 9 | return new JsonContactEmail(email.value(), email.type().name(), Util.getStringIfNotBlank(email.label())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonContactPhone.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | import org.asamk.signal.util.Util; 5 | 6 | public record JsonContactPhone(String value, String type, String label) { 7 | 8 | static JsonContactPhone from(MessageEnvelope.Data.SharedContact.Phone phone) { 9 | return new JsonContactPhone(phone.value(), phone.type().name(), Util.getStringIfNotBlank(phone.label())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogEntry.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.sendLog; 2 | 3 | import org.asamk.signal.manager.api.GroupId; 4 | import org.whispersystems.signalservice.api.crypto.ContentHint; 5 | import org.whispersystems.signalservice.internal.push.Content; 6 | 7 | import java.util.Optional; 8 | 9 | public record MessageSendLogEntry( 10 | Optional groupId, Content content, ContentHint contentHint, boolean urgent 11 | ) {} 12 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/RateLimitException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class RateLimitException extends Exception { 4 | 5 | private final long nextAttemptTimestamp; 6 | 7 | public RateLimitException(final long nextAttemptTimestamp) { 8 | super("Rate limit"); 9 | this.nextAttemptTimestamp = nextAttemptTimestamp; 10 | } 11 | 12 | public long getNextAttemptTimestamp() { 13 | return nextAttemptTimestamp; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/JsonRpcCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | 5 | import org.asamk.signal.OutputType; 6 | 7 | import java.util.List; 8 | 9 | public interface JsonRpcCommand extends Command { 10 | 11 | default TypeReference getRequestType() { 12 | return null; 13 | } 14 | 15 | default List getSupportedOutputTypes() { 16 | return List.of(OutputType.JSON); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/jsonrpc/JsonRpcBatchMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.jsonrpc; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | 5 | import java.util.List; 6 | 7 | public final class JsonRpcBatchMessage extends JsonRpcMessage { 8 | 9 | List messages; 10 | 11 | public JsonRpcBatchMessage(final List messages) { 12 | this.messages = messages; 13 | } 14 | 15 | public List getMessages() { 16 | return messages; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/MultiLocalCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Namespace; 4 | 5 | import org.asamk.signal.commands.exceptions.CommandException; 6 | import org.asamk.signal.manager.MultiAccountManager; 7 | import org.asamk.signal.output.OutputWriter; 8 | 9 | public interface MultiLocalCommand extends CliCommand { 10 | 11 | void handleCommand(Namespace ns, MultiAccountManager c, OutputWriter outputWriter) throws CommandException; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/CaptchaRejectedException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class CaptchaRejectedException extends Exception { 4 | 5 | public CaptchaRejectedException() { 6 | super("Captcha rejected"); 7 | } 8 | 9 | public CaptchaRejectedException(final String message) { 10 | super(message); 11 | } 12 | 13 | public CaptchaRejectedException(final String message, final Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageRecordUpdate.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.syncStorage; 2 | 3 | import org.whispersystems.signalservice.api.storage.SignalRecord; 4 | 5 | /** 6 | * Represents a pair of records: one old, and one new. The new record should replace the old. 7 | */ 8 | record StorageRecordUpdate>(E oldRecord, E newRecord) { 9 | 10 | @Override 11 | public String toString() { 12 | return newRecord.describeDiff(oldRecord); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/ProvisioningCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Namespace; 4 | 5 | import org.asamk.signal.commands.exceptions.CommandException; 6 | import org.asamk.signal.manager.ProvisioningManager; 7 | import org.asamk.signal.output.OutputWriter; 8 | 9 | public interface ProvisioningCommand extends CliCommand { 10 | 11 | void handleCommand(Namespace ns, ProvisioningManager m, final OutputWriter outputWriter) throws CommandException; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonEditMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.Manager; 4 | import org.asamk.signal.manager.api.MessageEnvelope; 5 | 6 | record JsonEditMessage(long targetSentTimestamp, JsonDataMessage dataMessage) { 7 | 8 | static JsonEditMessage from(MessageEnvelope.Edit editMessage, Manager m) { 9 | return new JsonEditMessage(editMessage.targetSentTimestamp(), 10 | JsonDataMessage.from(editMessage.dataMessage(), m)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager; 2 | 3 | import org.asamk.signal.manager.api.UserAlreadyExistsException; 4 | 5 | import java.io.IOException; 6 | import java.net.URI; 7 | import java.util.concurrent.TimeoutException; 8 | 9 | public interface ProvisioningManager { 10 | 11 | URI getDeviceLinkUri() throws TimeoutException, IOException; 12 | 13 | String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/UnregisteredRecipientException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class UnregisteredRecipientException extends Exception { 4 | 5 | private final RecipientAddress sender; 6 | 7 | public UnregisteredRecipientException(final RecipientAddress sender) { 8 | super("Unregistered user: " + sender.getIdentifier()); 9 | this.sender = sender; 10 | } 11 | 12 | public RecipientAddress getSender() { 13 | return sender; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonSticker.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | import org.asamk.signal.util.Hex; 5 | 6 | public record JsonSticker(String packId, int stickerId) { 7 | 8 | static JsonSticker from(MessageEnvelope.Data.Sticker sticker) { 9 | final var packId = Hex.toStringCondensed(sticker.packId().serialize()); 10 | final var stickerId = sticker.stickerId(); 11 | return new JsonSticker(packId, stickerId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/output/PlainTextWriter.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.output; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public non-sealed interface PlainTextWriter extends OutputWriter { 6 | 7 | void println(String format, Object... args); 8 | 9 | PlainTextWriter indentedWriter(); 10 | 11 | default void println() { 12 | println(""); 13 | } 14 | 15 | default void indent(final Consumer subWriter) { 16 | subWriter.accept(indentedWriter()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/PhoneNumberSharingMode.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public enum PhoneNumberSharingMode { 4 | EVERYBODY, 5 | CONTACTS, 6 | NOBODY; 7 | 8 | public static PhoneNumberSharingMode valueOfOrNull(String value) { 9 | if (value == null) { 10 | return null; 11 | } 12 | try { 13 | return valueOf(value); 14 | } catch (IllegalArgumentException ignored) { 15 | return null; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/TypingAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; 4 | 5 | public enum TypingAction { 6 | START, 7 | STOP; 8 | 9 | public SignalServiceTypingMessage.Action toSignalService() { 10 | return switch (this) { 11 | case START -> SignalServiceTypingMessage.Action.STARTED; 12 | case STOP -> SignalServiceTypingMessage.Action.STOPPED; 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessor.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.syncStorage; 2 | 3 | import org.whispersystems.signalservice.api.storage.SignalRecord; 4 | 5 | import java.sql.SQLException; 6 | 7 | /** 8 | * Handles processing a remote record, which involves applying any local changes that need to be 9 | * made based on the remote records. 10 | */ 11 | interface StorageRecordProcessor> { 12 | 13 | void process(E remoteRecord) throws SQLException; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/exceptions/CommandException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands.exceptions; 2 | 3 | public sealed abstract class CommandException extends Exception permits IOErrorException, RateLimitErrorException, UnexpectedErrorException, UntrustedKeyErrorException, UserErrorException { 4 | 5 | public CommandException(final String message) { 6 | super(message); 7 | } 8 | 9 | public CommandException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonPreview.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | public record JsonPreview(String url, String title, String description, JsonAttachment image) { 6 | 7 | static JsonPreview from(MessageEnvelope.Data.Preview preview) { 8 | return new JsonPreview(preview.url(), 9 | preview.title(), 10 | preview.description(), 11 | preview.image().map(JsonAttachment::from).orElse(null)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/internal/ReentrantSignalSessionLock.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.internal; 2 | 3 | import org.whispersystems.signalservice.api.SignalSessionLock; 4 | 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | class ReentrantSignalSessionLock implements SignalSessionLock { 8 | 9 | private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); 10 | 11 | @Override 12 | public Lock acquire() { 13 | LEGACY_LOCK.lock(); 14 | return LEGACY_LOCK::unlock; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/TrustNewIdentityCli.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal; 2 | 3 | public enum TrustNewIdentityCli { 4 | ALWAYS { 5 | @Override 6 | public String toString() { 7 | return "always"; 8 | } 9 | }, 10 | ON_FIRST_USE { 11 | @Override 12 | public String toString() { 13 | return "on-first-use"; 14 | } 15 | }, 16 | NEVER { 17 | @Override 18 | public String toString() { 19 | return "never"; 20 | } 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/ReceiveMode.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | enum ReceiveMode { 4 | ON_START { 5 | @Override 6 | public String toString() { 7 | return "on-start"; 8 | } 9 | }, 10 | ON_CONNECTION { 11 | @Override 12 | public String toString() { 13 | return "on-connection"; 14 | } 15 | }, 16 | MANUAL { 17 | @Override 18 | public String toString() { 19 | return "manual"; 20 | } 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/config/WhisperTrustStore.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.config; 2 | 3 | import org.whispersystems.signalservice.api.push.TrustStore; 4 | 5 | import java.io.InputStream; 6 | 7 | class WhisperTrustStore implements TrustStore { 8 | 9 | @Override 10 | public InputStream getKeyStoreInputStream() { 11 | return WhisperTrustStore.class.getResourceAsStream("whisper.store"); 12 | } 13 | 14 | @Override 15 | public String getKeyStorePassword() { 16 | return "whisper"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /man/Makefile: -------------------------------------------------------------------------------- 1 | A2X = a2x 2 | MKDIR = mkdir 3 | GZIP = gzip 4 | 5 | MANPAGESRC = signal-cli.1 signal-cli-dbus.5 signal-cli-jsonrpc.5 6 | 7 | .PHONY: all 8 | all: $(MANPAGESRC) 9 | 10 | %: %.adoc 11 | @echo "Generating manpage for $@" 12 | $(A2X) --no-xmllint --doctype manpage --format manpage "$^" 13 | 14 | .PHONY: install 15 | install: all 16 | $(MKDIR) -p man1 man5 17 | for f in *.1; do $(GZIP) < "$$f" > man1/"$$f".gz ; done 18 | for f in *.5; do $(GZIP) < "$$f" > man5/"$$f".gz ; done 19 | 20 | .PHONY: clean 21 | clean: 22 | rm -rf *.1 *.5 man1 man5 23 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/ServiceEnvironmentCli.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal; 2 | 3 | public enum ServiceEnvironmentCli { 4 | LIVE { 5 | @Override 6 | public String toString() { 7 | return "live"; 8 | } 9 | }, 10 | STAGING { 11 | @Override 12 | public String toString() { 13 | return "staging"; 14 | } 15 | }, 16 | @Deprecated SANDBOX { 17 | @Override 18 | public String toString() { 19 | return "sandbox"; 20 | } 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonRecipientAddress.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.RecipientAddress; 4 | 5 | import java.util.UUID; 6 | 7 | public record JsonRecipientAddress(String uuid, String number, String username) { 8 | 9 | public static JsonRecipientAddress from(RecipientAddress address) { 10 | return new JsonRecipientAddress(address.uuid().map(UUID::toString).orElse(null), 11 | address.number().orElse(null), 12 | address.username().orElse(null)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/Color.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public record Color(int color) { 4 | 5 | public int alpha() { 6 | return color >>> 24; 7 | } 8 | 9 | public int red() { 10 | return (color >> 16) & 0xFF; 11 | } 12 | 13 | public int green() { 14 | return (color >> 8) & 0xFF; 15 | } 16 | 17 | public int blue() { 18 | return color & 0xFF; 19 | } 20 | 21 | public String toHexColor() { 22 | return String.format("#%08x", color); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/jobs/CleanOldPreKeysJob.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.jobs; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class CleanOldPreKeysJob implements Job { 8 | 9 | private static final Logger logger = LoggerFactory.getLogger(CleanOldPreKeysJob.class); 10 | 11 | @Override 12 | public void run(Context context) { 13 | logger.trace("Cleaning old prekeys"); 14 | context.getPreKeyHelper().cleanOldPreKeys(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/azul/zulu-openjdk:21-jre-headless 2 | 3 | LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli 4 | LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger." 5 | LABEL org.opencontainers.image.licenses=GPL-3.0-only 6 | 7 | RUN useradd signal-cli --system --create-home --home-dir /var/lib/signal-cli 8 | ADD build/install/signal-cli /opt/signal-cli 9 | 10 | USER signal-cli 11 | ENTRYPOINT ["/opt/signal-cli/bin/signal-cli", "--config=/var/lib/signal-cli"] 12 | -------------------------------------------------------------------------------- /graalvm-config-dir/proxy-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "interfaces":["java.sql.Connection"] 4 | }, 5 | { 6 | "interfaces":["org.asamk.Signal"] 7 | }, 8 | { 9 | "interfaces":["org.asamk.Signal$Configuration"] 10 | }, 11 | { 12 | "interfaces":["org.asamk.Signal$Device"] 13 | }, 14 | { 15 | "interfaces":["org.asamk.Signal$Group"] 16 | }, 17 | { 18 | "interfaces":["org.asamk.Signal$Identity"] 19 | }, 20 | { 21 | "interfaces":["org.asamk.SignalControl"] 22 | }, 23 | { 24 | "interfaces":["org.freedesktop.dbus.interfaces.DBus"] 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /native.Containerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/debian:testing-slim 2 | 3 | LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli 4 | LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger." 5 | LABEL org.opencontainers.image.licenses=GPL-3.0-only 6 | 7 | RUN useradd signal-cli --system --create-home --home-dir /var/lib/signal-cli 8 | ADD build/native/nativeCompile/signal-cli /usr/bin/signal-cli 9 | 10 | USER signal-cli 11 | ENTRYPOINT ["/usr/bin/signal-cli", "--config=/var/lib/signal-cli"] 12 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyJsonContactsStore.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.contacts; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class LegacyJsonContactsStore { 9 | 10 | @JsonProperty("contacts") 11 | private final List contacts = new ArrayList<>(); 12 | 13 | private LegacyJsonContactsStore() { 14 | } 15 | 16 | public List getContacts() { 17 | return contacts; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/internal/PathConfig.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.internal; 2 | 3 | import java.io.File; 4 | 5 | public record PathConfig( 6 | File dataPath, File attachmentsPath, File avatarsPath, File stickerPacksPath 7 | ) { 8 | 9 | public static PathConfig createDefault(final File settingsPath) { 10 | return new PathConfig(new File(settingsPath, "data"), 11 | new File(settingsPath, "attachments"), 12 | new File(settingsPath, "avatars"), 13 | new File(settingsPath, "stickers")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonStoryContext.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | import java.util.UUID; 6 | 7 | record JsonStoryContext( 8 | String authorNumber, String authorUuid, long sentTimestamp 9 | ) { 10 | 11 | static JsonStoryContext from(MessageEnvelope.Data.StoryContext storyContext) { 12 | return new JsonStoryContext(storyContext.author().number().orElse(null), 13 | storyContext.author().uuid().map(UUID::toString).orElse(null), 14 | storyContext.sentTimestamp()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/GroupIdV1.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.Base64; 4 | 5 | import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes; 6 | 7 | public final class GroupIdV1 extends GroupId { 8 | 9 | public static GroupIdV1 createRandom() { 10 | return new GroupIdV1(getSecretBytes(16)); 11 | } 12 | 13 | public static GroupIdV1 fromBase64(String groupId) { 14 | return new GroupIdV1(Base64.getDecoder().decode(groupId)); 15 | } 16 | 17 | public GroupIdV1(final byte[] id) { 18 | super(id); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/UserAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.io.File; 4 | 5 | public class UserAlreadyExistsException extends Exception { 6 | 7 | private final String number; 8 | private final File fileName; 9 | 10 | public UserAlreadyExistsException(String number, File fileName) { 11 | this.number = number; 12 | this.fileName = fileName; 13 | } 14 | 15 | public String getNumber() { 16 | return number; 17 | } 18 | 19 | public File getFileName() { 20 | return fileName; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactsStore.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.contacts; 2 | 3 | import org.asamk.signal.manager.api.Contact; 4 | import org.asamk.signal.manager.api.Pair; 5 | import org.asamk.signal.manager.storage.recipients.RecipientId; 6 | 7 | import java.util.List; 8 | 9 | public interface ContactsStore { 10 | 11 | void storeContact(RecipientId recipientId, Contact contact); 12 | 13 | Contact getContact(RecipientId recipientId); 14 | 15 | List> getContacts(); 16 | 17 | void deleteContact(RecipientId recipientId); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public class SendSyncKeysAction implements HandleAction { 6 | 7 | private static final SendSyncKeysAction INSTANCE = new SendSyncKeysAction(); 8 | 9 | private SendSyncKeysAction() { 10 | } 11 | 12 | public static SendSyncKeysAction create() { 13 | return INSTANCE; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getSyncHelper().sendKeysMessage(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/stickerPacks/JsonStickerPack.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.stickerPacks; 2 | 3 | import org.asamk.signal.manager.api.StickerPack; 4 | 5 | import java.util.List; 6 | 7 | public record JsonStickerPack(String title, String author, JsonSticker cover, List stickers) { 8 | 9 | public record JsonSticker(Integer id, String emoji, String file, String contentType) { 10 | 11 | public StickerPack.Sticker toApi() { 12 | return new StickerPack.Sticker(id == null ? Integer.parseInt(file) : id, emoji, contentType); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.protocol; 2 | 3 | import org.asamk.signal.manager.storage.recipients.RecipientAddress; 4 | 5 | public class LegacySessionInfo { 6 | 7 | public final RecipientAddress address; 8 | 9 | public final int deviceId; 10 | 11 | public final byte[] sessionRecord; 12 | 13 | LegacySessionInfo(final RecipientAddress address, final int deviceId, final byte[] sessionRecord) { 14 | this.address = address; 15 | this.deviceId = deviceId; 16 | this.sessionRecord = sessionRecord; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public class SendSyncGroupsAction implements HandleAction { 6 | 7 | private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction(); 8 | 9 | private SendSyncGroupsAction() { 10 | } 11 | 12 | public static SendSyncGroupsAction create() { 13 | return INSTANCE; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getSyncHelper().sendGroups(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "signal-cli-client" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1" 10 | clap = { version = "4", features = ["cargo", "derive", "wrap_help"] } 11 | serde = "1" 12 | serde_json = "1" 13 | tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] } 14 | jsonrpsee = { version = "0.26", features = [ 15 | "macros", 16 | "async-client", 17 | "http-client", 18 | ] } 19 | bytes = "1" 20 | tokio-util = "0.7" 21 | futures-util = "0.3" 22 | thiserror = "2" 23 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonGroupInfo.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.Manager; 4 | import org.asamk.signal.manager.api.MessageEnvelope; 5 | 6 | record JsonGroupInfo(String groupId, String groupName, int revision, String type) { 7 | 8 | static JsonGroupInfo from(MessageEnvelope.Data.GroupContext groupContext, Manager m) { 9 | return new JsonGroupInfo(groupContext.groupId().toBase64(), 10 | m.getGroup(groupContext.groupId()).title(), 11 | groupContext.revision(), 12 | groupContext.isGroupUpdate() ? "UPDATE" : "DELIVER"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonPollCreate.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | import java.util.List; 6 | 7 | public record JsonPollCreate( 8 | String question, boolean allowMultiple, List options 9 | ) { 10 | 11 | static JsonPollCreate from(MessageEnvelope.Data.PollCreate pollCreate) { 12 | final var question = pollCreate.question(); 13 | final var allowMultiple = pollCreate.allowMultiple(); 14 | final var options = pollCreate.options(); 15 | 16 | return new JsonPollCreate(question, allowMultiple, options); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public class RefreshPreKeysAction implements HandleAction { 6 | 7 | private static final RefreshPreKeysAction INSTANCE = new RefreshPreKeysAction(); 8 | 9 | private RefreshPreKeysAction() { 10 | } 11 | 12 | public static RefreshPreKeysAction create() { 13 | return INSTANCE; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getPreKeyHelper().refreshPreKeysIfNecessary(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public class SendSyncContactsAction implements HandleAction { 6 | 7 | private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction(); 8 | 9 | private SendSyncContactsAction() { 10 | } 11 | 12 | public static SendSyncContactsAction create() { 13 | return INSTANCE; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getSyncHelper().sendContacts(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public class SendSyncBlockedListAction implements HandleAction { 6 | 7 | private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction(); 8 | 9 | private SendSyncBlockedListAction() { 10 | } 11 | 12 | public static SendSyncBlockedListAction create() { 13 | return INSTANCE; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getSyncHelper().sendBlockedList(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/util/DateUtils.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.util; 2 | 3 | import java.text.DateFormat; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Date; 6 | import java.util.TimeZone; 7 | 8 | public class DateUtils { 9 | 10 | private static final TimeZone tzUTC = TimeZone.getTimeZone("UTC"); 11 | 12 | private DateUtils() { 13 | } 14 | 15 | public static String formatTimestamp(long timestamp) { 16 | var date = new Date(timestamp); 17 | final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"); 18 | df.setTimeZone(tzUTC); 19 | return timestamp + " (" + df.format(date) + ")"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/org.asamk.Signal.conf: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonMention.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | import java.util.UUID; 6 | 7 | public record JsonMention(@Deprecated String name, String number, String uuid, int start, int length) { 8 | 9 | static JsonMention from(MessageEnvelope.Data.Mention mention) { 10 | final var address = mention.recipient(); 11 | return new JsonMention(address.getLegacyIdentifier(), 12 | address.number().orElse(null), 13 | address.uuid().map(UUID::toString).orElse(null), 14 | mention.start(), 15 | mention.length()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public class SendSyncConfigurationAction implements HandleAction { 6 | 7 | private static final SendSyncConfigurationAction INSTANCE = new SendSyncConfigurationAction(); 8 | 9 | private SendSyncConfigurationAction() { 10 | } 11 | 12 | public static SendSyncConfigurationAction create() { 13 | return INSTANCE; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getSyncHelper().sendConfigurationMessage(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | 5 | public class UpdateAccountAttributesAction implements HandleAction { 6 | 7 | private static final UpdateAccountAttributesAction INSTANCE = new UpdateAccountAttributesAction(); 8 | 9 | private UpdateAccountAttributesAction() { 10 | } 11 | 12 | public static UpdateAccountAttributesAction create() { 13 | return INSTANCE; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getAccountHelper().updateAccountAttributes(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/UntrustedIdentityException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class UntrustedIdentityException extends Exception { 4 | 5 | private final RecipientAddress sender; 6 | private final int senderDevice; 7 | 8 | public UntrustedIdentityException(final RecipientAddress sender, final int senderDevice) { 9 | super("Untrusted identity: " + sender.getIdentifier()); 10 | this.sender = sender; 11 | this.senderDevice = senderDevice; 12 | } 13 | 14 | public RecipientAddress getSender() { 15 | return sender; 16 | } 17 | 18 | public int getSenderDevice() { 19 | return senderDevice; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SyncStorageDataAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.asamk.signal.manager.jobs.SyncStorageJob; 5 | 6 | public class SyncStorageDataAction implements HandleAction { 7 | 8 | private static final SyncStorageDataAction INSTANCE = new SyncStorageDataAction(); 9 | 10 | private SyncStorageDataAction() { 11 | } 12 | 13 | public static SyncStorageDataAction create() { 14 | return INSTANCE; 15 | } 16 | 17 | @Override 18 | public void execute(Context context) throws Throwable { 19 | context.getJobExecutor().enqueueJob(new SyncStorageJob()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/jobs/CheckWhoAmIJob.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.jobs; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.io.IOException; 8 | 9 | public class CheckWhoAmIJob implements Job { 10 | 11 | private static final Logger logger = LoggerFactory.getLogger(CheckWhoAmIJob.class); 12 | 13 | @Override 14 | public void run(Context context) { 15 | logger.trace("Checking whoAmI"); 16 | try { 17 | context.getAccountHelper().checkWhoAmiI(); 18 | } catch (IOException e) { 19 | logger.warn("Failed to check whoAmI", e); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/StickerPack.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public record StickerPack( 7 | StickerPackId packId, 8 | StickerPackUrl url, 9 | boolean installed, 10 | String title, 11 | String author, 12 | Optional cover, 13 | List stickers 14 | ) { 15 | 16 | public StickerPack(final StickerPackId packId, final byte[] packKey, final boolean installed) { 17 | this(packId, new StickerPackUrl(packId, packKey), installed, "", "", Optional.empty(), List.of()); 18 | } 19 | 20 | public record Sticker(int id, String emoji, String contentType) {} 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.config; 2 | 3 | import org.asamk.signal.manager.api.ServiceEnvironment; 4 | import org.signal.libsignal.net.Network; 5 | import org.signal.libsignal.protocol.ecc.ECPublicKey; 6 | import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; 7 | 8 | import java.util.List; 9 | 10 | public record ServiceEnvironmentConfig( 11 | ServiceEnvironment type, 12 | Network.Environment netEnvironment, 13 | SignalServiceConfiguration signalServiceConfiguration, 14 | List unidentifiedSenderTrustRoots, 15 | String cdsiMrenclave, 16 | List svr2Mrenclaves 17 | ) {} 18 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/CaptchaRequiredException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | public class CaptchaRequiredException extends Exception { 4 | 5 | private long nextAttemptTimestamp; 6 | 7 | public CaptchaRequiredException(final long nextAttemptTimestamp) { 8 | super("Captcha required"); 9 | this.nextAttemptTimestamp = nextAttemptTimestamp; 10 | } 11 | 12 | public CaptchaRequiredException(final String message) { 13 | super(message); 14 | } 15 | 16 | public CaptchaRequiredException(final String message, final Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public long getNextAttemptTimestamp() { 21 | return nextAttemptTimestamp; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonTypingMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | 5 | import org.asamk.signal.manager.api.GroupId; 6 | import org.asamk.signal.manager.api.MessageEnvelope; 7 | 8 | record JsonTypingMessage( 9 | String action, long timestamp, @JsonInclude(JsonInclude.Include.NON_NULL) String groupId 10 | ) { 11 | 12 | static JsonTypingMessage from(MessageEnvelope.Typing typingMessage) { 13 | final var action = typingMessage.type().name(); 14 | final var timestamp = typingMessage.timestamp(); 15 | final var groupId = typingMessage.groupId().map(GroupId::toBase64).orElse(null); 16 | return new JsonTypingMessage(action, timestamp, groupId); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/transports/tcp.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error; 2 | 3 | use futures_util::stream::StreamExt; 4 | use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT}; 5 | use tokio::net::{TcpStream, ToSocketAddrs}; 6 | use tokio_util::codec::Decoder; 7 | 8 | use super::stream_codec::StreamCodec; 9 | use super::{Receiver, Sender}; 10 | 11 | /// Connect to a JSON-RPC TCP server. 12 | pub async fn connect( 13 | socket: impl ToSocketAddrs, 14 | ) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> { 15 | let connection = TcpStream::connect(socket).await?; 16 | let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split(); 17 | 18 | let sender = Sender { inner: sink }; 19 | let receiver = Receiver { inner: stream }; 20 | 21 | Ok((sender, receiver)) 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonContactName.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | import org.asamk.signal.util.Util; 5 | 6 | public record JsonContactName( 7 | String nickname, String given, String family, String prefix, String suffix, String middle 8 | ) { 9 | 10 | static JsonContactName from(MessageEnvelope.Data.SharedContact.Name name) { 11 | return new JsonContactName(Util.getStringIfNotBlank(name.nickname()), 12 | Util.getStringIfNotBlank(name.given()), 13 | Util.getStringIfNotBlank(name.family()), 14 | Util.getStringIfNotBlank(name.prefix()), 15 | Util.getStringIfNotBlank(name.suffix()), 16 | Util.getStringIfNotBlank(name.middle())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/transports/ipc.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error; 2 | use std::path::Path; 3 | 4 | use futures_util::stream::StreamExt; 5 | use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT}; 6 | use tokio::net::UnixStream; 7 | use tokio_util::codec::Decoder; 8 | 9 | use super::stream_codec::StreamCodec; 10 | use super::{Receiver, Sender}; 11 | 12 | /// Connect to a JSON-RPC Unix Socket server. 13 | pub async fn connect( 14 | socket: impl AsRef, 15 | ) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> { 16 | let connection = UnixStream::connect(socket).await?; 17 | let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split(); 18 | 19 | let sender = Sender { inner: sink }; 20 | let receiver = Receiver { inner: stream }; 21 | 22 | Ok((sender, receiver)) 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/BaseConfig.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal; 2 | 3 | import java.util.Optional; 4 | 5 | public class BaseConfig { 6 | 7 | public static final String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle(); 8 | public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); 9 | 10 | static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT")) 11 | .orElse("Signal-Android/7.63.2"); 12 | static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null 13 | ? "signal-cli" 14 | : PROJECT_NAME + "/" + PROJECT_VERSION; 15 | static final String USER_AGENT = USER_AGENT_SIGNAL_ANDROID + " " + USER_AGENT_SIGNAL_CLI; 16 | 17 | private BaseConfig() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | import java.util.UUID; 6 | 7 | record JsonSyncReadMessage( 8 | @Deprecated String sender, String senderNumber, String senderUuid, long timestamp 9 | ) { 10 | 11 | static JsonSyncReadMessage from(MessageEnvelope.Sync.Read readMessage) { 12 | final var senderAddress = readMessage.sender(); 13 | final var sender = senderAddress.getLegacyIdentifier(); 14 | final var senderNumber = senderAddress.number().orElse(null); 15 | final var senderUuid = senderAddress.uuid().map(UUID::toString).orElse(null); 16 | final var timestamp = readMessage.timestamp(); 17 | return new JsonSyncReadMessage(sender, senderNumber, senderUuid, timestamp); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/jsonrpc/JsonRpcSender.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.jsonrpc; 2 | 3 | import org.asamk.signal.output.JsonWriter; 4 | 5 | import java.util.List; 6 | 7 | public class JsonRpcSender { 8 | 9 | private final JsonWriter jsonWriter; 10 | 11 | public JsonRpcSender(final JsonWriter jsonWriter) { 12 | this.jsonWriter = jsonWriter; 13 | } 14 | 15 | public void sendRequest(JsonRpcRequest request) { 16 | jsonWriter.write(request); 17 | } 18 | 19 | public void sendBatchRequests(List requests) { 20 | jsonWriter.write(requests); 21 | } 22 | 23 | public void sendResponse(JsonRpcResponse response) { 24 | jsonWriter.write(response); 25 | } 26 | 27 | public void sendBatchResponses(List responses) { 28 | jsonWriter.write(responses); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | 5 | import org.asamk.signal.manager.api.MessageEnvelope; 6 | 7 | public record JsonQuotedAttachment( 8 | String contentType, String filename, @JsonInclude(JsonInclude.Include.NON_NULL) JsonAttachment thumbnail 9 | ) { 10 | 11 | static JsonQuotedAttachment from(MessageEnvelope.Data.Attachment quotedAttachment) { 12 | final var contentType = quotedAttachment.contentType(); 13 | final var filename = quotedAttachment.fileName().orElse(null); 14 | final var thumbnail = quotedAttachment.thumbnail().isPresent() 15 | ? JsonAttachment.from(quotedAttachment.thumbnail().get()) 16 | : null; 17 | return new JsonQuotedAttachment(contentType, filename, thumbnail); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonReceiptMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | import java.util.List; 6 | 7 | record JsonReceiptMessage(long when, boolean isDelivery, boolean isRead, boolean isViewed, List timestamps) { 8 | 9 | static JsonReceiptMessage from(MessageEnvelope.Receipt receiptMessage) { 10 | final var when = receiptMessage.when(); 11 | final var isDelivery = receiptMessage.type() == MessageEnvelope.Receipt.Type.DELIVERY; 12 | final var isRead = receiptMessage.type() == MessageEnvelope.Receipt.Type.READ; 13 | final var isViewed = receiptMessage.type() == MessageEnvelope.Receipt.Type.VIEWED; 14 | final var timestamps = receiptMessage.timestamps(); 15 | return new JsonReceiptMessage(when, isDelivery, isRead, isViewed, timestamps); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/jobs/RefreshRecipientsJob.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.jobs; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class RefreshRecipientsJob implements Job { 8 | 9 | private static final Logger logger = LoggerFactory.getLogger(RefreshRecipientsJob.class); 10 | 11 | @Override 12 | public void run(Context context) { 13 | logger.trace("Full CDSI recipients refresh"); 14 | try { 15 | context.getRecipientHelper().refreshUsers(); 16 | } catch (Exception e) { 17 | logger.warn("Full CDSI recipients refresh failed, ignoring: {} ({})", 18 | e.getMessage(), 19 | e.getClass().getSimpleName()); 20 | logger.debug("Full CDSI refresh failed", e); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/Configuration.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import org.asamk.signal.manager.storage.configuration.ConfigurationStore; 4 | 5 | import java.util.Optional; 6 | 7 | public record Configuration( 8 | Optional readReceipts, 9 | Optional unidentifiedDeliveryIndicators, 10 | Optional typingIndicators, 11 | Optional linkPreviews 12 | ) { 13 | 14 | public static Configuration from(final ConfigurationStore configurationStore) { 15 | return new Configuration(Optional.ofNullable(configurationStore.getReadReceipts()), 16 | Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()), 17 | Optional.ofNullable(configurationStore.getTypingIndicators()), 18 | Optional.ofNullable(configurationStore.getLinkPreviews())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.jobs; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class DownloadProfileAvatarJob implements Job { 8 | 9 | private static final Logger logger = LoggerFactory.getLogger(DownloadProfileAvatarJob.class); 10 | private final String avatarPath; 11 | 12 | public DownloadProfileAvatarJob(final String avatarPath) { 13 | this.avatarPath = avatarPath; 14 | } 15 | 16 | @Override 17 | public void run(Context context) { 18 | logger.trace("Downloading profile avatar {}", avatarPath); 19 | final var account = context.getAccount(); 20 | context.getProfileHelper() 21 | .downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, account.getProfileKey()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/MultiAccountManager.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | import java.util.List; 6 | import java.util.concurrent.TimeoutException; 7 | import java.util.function.Consumer; 8 | 9 | public interface MultiAccountManager extends AutoCloseable { 10 | 11 | List getAccountNumbers(); 12 | 13 | List getManagers(); 14 | 15 | void addOnManagerAddedHandler(Consumer handler); 16 | 17 | void addOnManagerRemovedHandler(Consumer handler); 18 | 19 | Manager getManager(String phoneNumber); 20 | 21 | URI getNewProvisioningDeviceLinkUri() throws TimeoutException, IOException; 22 | 23 | ProvisioningManager getProvisioningManagerFor(URI deviceLinkUri); 24 | 25 | RegistrationManager getNewRegistrationManager(String account) throws IOException; 26 | 27 | @Override 28 | void close(); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.jobs; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.asamk.signal.manager.storage.recipients.RecipientAddress; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public class DownloadProfileJob implements Job { 9 | 10 | private static final Logger logger = LoggerFactory.getLogger(DownloadProfileJob.class); 11 | private final RecipientAddress address; 12 | 13 | public DownloadProfileJob(RecipientAddress address) { 14 | this.address = address; 15 | } 16 | 17 | @Override 18 | public void run(Context context) { 19 | logger.trace("Refreshing profile for {}", address); 20 | final var account = context.getAccount(); 21 | final var recipientId = account.getRecipientStore().resolveRecipient(address); 22 | context.getProfileHelper().refreshRecipientProfile(recipientId); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.profiles; 2 | 3 | import org.asamk.signal.manager.api.Profile; 4 | import org.asamk.signal.manager.storage.recipients.RecipientId; 5 | import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; 6 | import org.signal.libsignal.zkgroup.profiles.ProfileKey; 7 | 8 | public interface ProfileStore { 9 | 10 | Profile getProfile(RecipientId recipientId); 11 | 12 | ProfileKey getProfileKey(RecipientId recipientId); 13 | 14 | ExpiringProfileKeyCredential getExpiringProfileKeyCredential(RecipientId recipientId); 15 | 16 | void storeProfile(RecipientId recipientId, Profile profile); 17 | 18 | void storeProfileKey(RecipientId recipientId, ProfileKey profileKey); 19 | 20 | void storeExpiringProfileKeyCredential( 21 | RecipientId recipientId, 22 | ExpiringProfileKeyCredential expiringProfileKeyCredential 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/internal/AccountFileUpdaterImpl.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.internal; 2 | 3 | import org.asamk.signal.manager.helper.AccountFileUpdater; 4 | import org.asamk.signal.manager.storage.accounts.AccountsStore; 5 | import org.signal.core.models.ServiceId.ACI; 6 | 7 | public class AccountFileUpdaterImpl implements AccountFileUpdater { 8 | 9 | private final AccountsStore accountsStore; 10 | private final String accountPath; 11 | 12 | public AccountFileUpdaterImpl(final AccountsStore accountsStore, final String accountPath) { 13 | this.accountsStore = accountsStore; 14 | this.accountPath = accountPath; 15 | } 16 | 17 | @Override 18 | public void updateAccountIdentifiers(final String newNumber, final ACI newAci) { 19 | accountsStore.updateAccount(accountPath, newNumber, newAci); 20 | } 21 | 22 | @Override 23 | public void removeAccount() { 24 | accountsStore.removeAccount(accountPath); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | slf4j = "2.0.17" 3 | junit = "6.0.1" 4 | 5 | [libraries] 6 | bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.82" 7 | jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.1" 8 | argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0" 9 | dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0" 10 | slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } 11 | slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } 12 | logback = "ch.qos.logback:logback-classic:1.5.21" 13 | 14 | signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_135" 15 | sqlite = "org.xerial:sqlite-jdbc:3.51.0.0" 16 | hikari = "com.zaxxer:HikariCP:7.0.2" 17 | junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } 18 | junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } 19 | junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Question 2 | 3 | If you have a question you can ask it in the [GitHub discussions page](https://github.com/AsamK/signal-cli/discussions) 4 | 5 | # Report a bug 6 | 7 | - Search [existing issues](https://github.com/AsamK/signal-cli/issues?q=is%3Aissue) if it has been reported already 8 | - If you're unable to find an open issue addressing the 9 | problem, [open a new one](https://github.com/AsamK/signal-cli/issues/new). 10 | - Be sure to include a **title and clear description**, as much relevant information as possible. 11 | - Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using 12 | - Specify if it's the normal java or the graalvm native version. 13 | - Run the failing command with `--verbose` flag to get a more detailed log output and include that in the bug report 14 | 15 | # Pull request 16 | 17 | - Code style should match the existing code, IntelliJ users can use the auto formatter 18 | - Separate PRs should be opened for each implemented feature or bug fix 19 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/DbusConfig.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal; 2 | 3 | import org.asamk.signal.dbus.DbusUtils; 4 | 5 | import java.io.File; 6 | 7 | public class DbusConfig { 8 | 9 | private static final String SIGNAL_BUSNAME = "org.asamk.Signal"; 10 | private static final String SIGNAL_BUSNAME_FLATPAK = "org.asamk.SignalCli"; 11 | private static final String SIGNAL_OBJECT_BASE_PATH = "/org/asamk/Signal"; 12 | 13 | public static String getBusname() { 14 | if (new File("/.flatpak-info").exists()) { 15 | return SIGNAL_BUSNAME_FLATPAK; 16 | } else { 17 | return SIGNAL_BUSNAME; 18 | } 19 | } 20 | 21 | public static String getObjectPath() { 22 | return getObjectPath(null); 23 | } 24 | 25 | public static String getObjectPath(String account) { 26 | if (account == null) { 27 | return SIGNAL_OBJECT_BASE_PATH; 28 | } 29 | 30 | return SIGNAL_OBJECT_BASE_PATH + "/" + DbusUtils.makeValidObjectPathElement(account); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/syncStorage/WriteOperationResult.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.syncStorage; 2 | 3 | import org.whispersystems.signalservice.api.storage.SignalStorageManifest; 4 | import org.whispersystems.signalservice.api.storage.SignalStorageRecord; 5 | 6 | import java.util.List; 7 | import java.util.Locale; 8 | 9 | public record WriteOperationResult( 10 | SignalStorageManifest manifest, List inserts, List deletes 11 | ) { 12 | 13 | public boolean isEmpty() { 14 | return inserts.isEmpty() && deletes.isEmpty(); 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | if (isEmpty()) { 20 | return "Empty"; 21 | } else { 22 | return String.format(Locale.ROOT, 23 | "ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d", 24 | manifest.version, 25 | manifest.storageIds.size(), 26 | inserts.size(), 27 | deletes.size()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonSyncStoryMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 4 | 5 | import org.asamk.signal.manager.api.MessageEnvelope; 6 | 7 | import java.util.UUID; 8 | 9 | record JsonSyncStoryMessage( 10 | String destinationNumber, String destinationUuid, @JsonUnwrapped JsonStoryMessage dataMessage 11 | ) { 12 | 13 | static JsonSyncStoryMessage from(MessageEnvelope.Sync.Sent transcriptMessage) { 14 | if (transcriptMessage.destination().isPresent()) { 15 | final var address = transcriptMessage.destination().get(); 16 | return new JsonSyncStoryMessage(address.number().orElse(null), 17 | address.uuid().map(UUID::toString).orElse(null), 18 | transcriptMessage.story().map(JsonStoryMessage::from).orElse(null)); 19 | 20 | } else { 21 | return new JsonSyncStoryMessage(null, 22 | null, 23 | transcriptMessage.story().map(JsonStoryMessage::from).orElse(null)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/StickerPackId.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import org.signal.core.util.Hex; 4 | 5 | import java.util.Arrays; 6 | 7 | public class StickerPackId { 8 | 9 | private final byte[] id; 10 | 11 | private StickerPackId(final byte[] id) { 12 | this.id = id; 13 | } 14 | 15 | public static StickerPackId deserialize(byte[] packId) { 16 | return new StickerPackId(packId); 17 | } 18 | 19 | public byte[] serialize() { 20 | return id; 21 | } 22 | 23 | @Override 24 | public boolean equals(final Object o) { 25 | if (this == o) return true; 26 | if (o == null || getClass() != o.getClass()) return false; 27 | 28 | final StickerPackId that = (StickerPackId) o; 29 | 30 | return Arrays.equals(id, that.id); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Arrays.hashCode(id); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "StickerPackId{" + Hex.toStringCondensed(id) + '}'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.asamk.signal.manager.storage.recipients.RecipientId; 5 | 6 | public class RetrieveProfileAction implements HandleAction { 7 | 8 | private final RecipientId recipientId; 9 | 10 | public RetrieveProfileAction(final RecipientId recipientId) { 11 | this.recipientId = recipientId; 12 | } 13 | 14 | @Override 15 | public void execute(Context context) throws Throwable { 16 | context.getProfileHelper().refreshRecipientProfile(recipientId); 17 | } 18 | 19 | @Override 20 | public boolean equals(final Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | final RetrieveProfileAction that = (RetrieveProfileAction) o; 25 | 26 | return recipientId.equals(that.recipientId); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return recipientId.hashCode(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.asamk.signal.manager.storage.recipients.RecipientId; 5 | 6 | import java.util.Objects; 7 | 8 | public class SendProfileKeyAction implements HandleAction { 9 | 10 | private final RecipientId recipientId; 11 | 12 | public SendProfileKeyAction(final RecipientId recipientId) { 13 | this.recipientId = recipientId; 14 | } 15 | 16 | @Override 17 | public void execute(Context context) throws Throwable { 18 | context.getSendHelper().sendProfileKey(recipientId); 19 | } 20 | 21 | @Override 22 | public boolean equals(final Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | final SendProfileKeyAction that = (SendProfileKeyAction) o; 26 | return recipientId.equals(that.recipientId); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(recipientId); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonPollVote.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | public record JsonPollVote( 9 | @Deprecated String author, 10 | String authorNumber, 11 | String authorUuid, 12 | long targetSentTimestamp, 13 | List optionIndexes, 14 | int voteCount 15 | ) { 16 | 17 | static JsonPollVote from(MessageEnvelope.Data.PollVote pollVote) { 18 | final var address = pollVote.targetAuthor(); 19 | final var author = address.getLegacyIdentifier(); 20 | final var authorNumber = address.number().orElse(null); 21 | final var authorUuid = address.uuid().map(UUID::toString).orElse(null); 22 | final var targetSentTimestamp = pollVote.targetSentTimestamp(); 23 | final var optionIndexes = pollVote.optionIndexes(); 24 | final var voteCount = pollVote.voteCount(); 25 | 26 | return new JsonPollVote(author, authorNumber, authorUuid, targetSentTimestamp, optionIndexes, voteCount); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.jobs; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.io.IOException; 8 | 9 | public class SyncStorageJob implements Job { 10 | 11 | private final boolean forcePush; 12 | 13 | private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class); 14 | 15 | public SyncStorageJob() { 16 | this.forcePush = false; 17 | } 18 | 19 | public SyncStorageJob(final boolean forcePush) { 20 | this.forcePush = forcePush; 21 | } 22 | 23 | @Override 24 | public void run(Context context) { 25 | logger.trace("Running storage sync job"); 26 | try { 27 | if (forcePush) { 28 | context.getStorageHelper().forcePushToStorage(); 29 | } else { 30 | context.getStorageHelper().syncDataWithStorage(); 31 | } 32 | } catch (IOException e) { 33 | logger.warn("Failed to sync storage data", e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | 5 | import net.sourceforge.argparse4j.inf.Namespace; 6 | 7 | import org.asamk.signal.OutputType; 8 | import org.asamk.signal.commands.exceptions.CommandException; 9 | import org.asamk.signal.manager.Manager; 10 | import org.asamk.signal.output.JsonWriter; 11 | 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | public interface JsonRpcLocalCommand extends JsonRpcSingleCommand>, LocalCommand { 16 | 17 | default TypeReference> getRequestType() { 18 | return new TypeReference<>() {}; 19 | } 20 | 21 | default void handleCommand(Map request, Manager m, JsonWriter jsonWriter) throws CommandException { 22 | Namespace commandNamespace = new JsonRpcNamespace(request == null ? Map.of() : request); 23 | handleCommand(commandNamespace, m, jsonWriter); 24 | } 25 | 26 | default List getSupportedOutputTypes() { 27 | return List.of(OutputType.PLAIN_TEXT, OutputType.JSON); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/dbus/DbusProperty.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.dbus; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Supplier; 5 | 6 | public class DbusProperty { 7 | 8 | private final String name; 9 | private final Supplier getter; 10 | private final Consumer setter; 11 | 12 | public DbusProperty(final String name, final Supplier getter, final Consumer setter) { 13 | this.name = name; 14 | this.getter = getter; 15 | this.setter = setter; 16 | } 17 | 18 | public DbusProperty(final String name, final Supplier getter) { 19 | this.name = name; 20 | this.getter = getter; 21 | this.setter = null; 22 | } 23 | 24 | public DbusProperty(final String name, final Consumer setter) { 25 | this.name = name; 26 | this.getter = null; 27 | this.setter = setter; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public Consumer getSetter() { 35 | return setter; 36 | } 37 | 38 | public Supplier getGetter() { 39 | return getter; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.groups; 2 | 3 | import org.asamk.signal.manager.util.KeyUtils; 4 | 5 | import java.util.Arrays; 6 | 7 | public final class GroupLinkPassword { 8 | 9 | private static final int SIZE = 16; 10 | 11 | private final byte[] bytes; 12 | 13 | public static GroupLinkPassword createNew() { 14 | return new GroupLinkPassword(KeyUtils.getSecretBytes(SIZE)); 15 | } 16 | 17 | public static GroupLinkPassword fromBytes(byte[] bytes) { 18 | return new GroupLinkPassword(bytes); 19 | } 20 | 21 | private GroupLinkPassword(byte[] bytes) { 22 | this.bytes = bytes; 23 | } 24 | 25 | public byte[] serialize() { 26 | return bytes.clone(); 27 | } 28 | 29 | @Override 30 | public boolean equals(Object other) { 31 | if (!(other instanceof GroupLinkPassword)) { 32 | return false; 33 | } 34 | 35 | return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return Arrays.hashCode(bytes); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonContactAddress.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | import org.asamk.signal.util.Util; 5 | 6 | public record JsonContactAddress( 7 | String type, 8 | String label, 9 | String street, 10 | String pobox, 11 | String neighborhood, 12 | String city, 13 | String region, 14 | String postcode, 15 | String country 16 | ) { 17 | 18 | static JsonContactAddress from(MessageEnvelope.Data.SharedContact.Address address) { 19 | return new JsonContactAddress(address.type().name(), 20 | Util.getStringIfNotBlank(address.label()), 21 | Util.getStringIfNotBlank(address.street()), 22 | Util.getStringIfNotBlank(address.pobox()), 23 | Util.getStringIfNotBlank(address.neighborhood()), 24 | Util.getStringIfNotBlank(address.city()), 25 | Util.getStringIfNotBlank(address.region()), 26 | Util.getStringIfNotBlank(address.postcode()), 27 | Util.getStringIfNotBlank(address.country())); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/util/RandomUtils.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.util; 2 | 3 | import java.security.NoSuchAlgorithmException; 4 | import java.security.SecureRandom; 5 | 6 | public class RandomUtils { 7 | 8 | private static final ThreadLocal LOCAL_RANDOM = ThreadLocal.withInitial(() -> { 9 | var rand = getSecureRandomUnseeded(); 10 | 11 | // Let the SecureRandom seed itself initially 12 | rand.nextBoolean(); 13 | 14 | return rand; 15 | }); 16 | 17 | private static SecureRandom getSecureRandomUnseeded() { 18 | try { 19 | return SecureRandom.getInstance("NativePRNG"); 20 | } catch (NoSuchAlgorithmException e) { 21 | // Fallback to SHA1PRNG if NativePRNG is not available (e.g. on windows) 22 | try { 23 | return SecureRandom.getInstance("SHA1PRNG"); 24 | } catch (NoSuchAlgorithmException e1) { 25 | // Fallback to default 26 | return new SecureRandom(); 27 | } 28 | } 29 | } 30 | 31 | public static SecureRandom getSecureRandom() { 32 | return LOCAL_RANDOM.get(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/IdentityVerificationCode.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import org.signal.libsignal.protocol.util.Hex; 4 | 5 | import java.util.Base64; 6 | import java.util.Locale; 7 | 8 | public sealed interface IdentityVerificationCode { 9 | 10 | record Fingerprint(byte[] fingerprint) implements IdentityVerificationCode {} 11 | 12 | record SafetyNumber(String safetyNumber) implements IdentityVerificationCode {} 13 | 14 | record ScannableSafetyNumber(byte[] safetyNumber) implements IdentityVerificationCode {} 15 | 16 | static IdentityVerificationCode parse(String code) throws Exception { 17 | code = code.replaceAll(" ", ""); 18 | if (code.length() == 66) { 19 | final var fingerprintBytes = Hex.fromStringCondensed(code.toLowerCase(Locale.ROOT)); 20 | return new Fingerprint(fingerprintBytes); 21 | } else if (code.length() == 60) { 22 | return new SafetyNumber(code); 23 | } else { 24 | final var scannableSafetyNumber = Base64.getDecoder().decode(code); 25 | return new ScannableSafetyNumber(scannableSafetyNumber); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/output/JsonWriterImpl.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.output; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import org.asamk.signal.util.Util; 7 | 8 | import java.io.IOException; 9 | import java.io.Writer; 10 | 11 | public class JsonWriterImpl implements JsonWriter { 12 | 13 | private final Writer writer; 14 | private final ObjectMapper objectMapper; 15 | 16 | public JsonWriterImpl(final Writer writer) { 17 | this.writer = writer; 18 | this.objectMapper = Util.createJsonObjectMapper(); 19 | } 20 | 21 | public synchronized void write(final Object object) { 22 | try { 23 | try { 24 | objectMapper.writeValue(writer, object); 25 | } catch (JsonProcessingException e) { 26 | // Some issue with json serialization, probably caused by a bug 27 | throw new AssertionError(e); 28 | } 29 | writer.write(System.lineSeparator()); 30 | writer.flush(); 31 | } catch (IOException e) { 32 | throw new AssertionError(e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonReaction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | import java.util.UUID; 6 | 7 | public record JsonReaction( 8 | String emoji, 9 | @Deprecated String targetAuthor, 10 | String targetAuthorNumber, 11 | String targetAuthorUuid, 12 | long targetSentTimestamp, 13 | boolean isRemove 14 | ) { 15 | 16 | static JsonReaction from(MessageEnvelope.Data.Reaction reaction) { 17 | final var emoji = reaction.emoji(); 18 | final var address = reaction.targetAuthor(); 19 | final var targetAuthor = address.getLegacyIdentifier(); 20 | final var targetAuthorNumber = address.number().orElse(null); 21 | final var targetAuthorUuid = address.uuid().map(UUID::toString).orElse(null); 22 | final var targetSentTimestamp = reaction.targetSentTimestamp(); 23 | final var isRemove = reaction.isRemove(); 24 | return new JsonReaction(emoji, 25 | targetAuthor, 26 | targetAuthorNumber, 27 | targetAuthorUuid, 28 | targetSentTimestamp, 29 | isRemove); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonAttachment.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.api.MessageEnvelope; 4 | 5 | record JsonAttachment( 6 | String contentType, 7 | String filename, 8 | String id, 9 | Long size, 10 | Integer width, 11 | Integer height, 12 | String caption, 13 | Long uploadTimestamp 14 | ) { 15 | 16 | static JsonAttachment from(MessageEnvelope.Data.Attachment attachment) { 17 | final var id = attachment.id().orElse(null); 18 | final var filename = attachment.fileName().orElse(null); 19 | final var size = attachment.size().orElse(null); 20 | final var width = attachment.width().orElse(null); 21 | final var height = attachment.height().orElse(null); 22 | final var caption = attachment.caption().orElse(null); 23 | final var uploadTimestamp = attachment.uploadTimestamp().orElse(null); 24 | 25 | return new JsonAttachment(attachment.contentType(), 26 | filename, 27 | id, 28 | size, 29 | width, 30 | height, 31 | caption, 32 | uploadTimestamp); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/JsonRpcMultiLocalCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | 5 | import net.sourceforge.argparse4j.inf.Namespace; 6 | 7 | import org.asamk.signal.OutputType; 8 | import org.asamk.signal.commands.exceptions.CommandException; 9 | import org.asamk.signal.manager.MultiAccountManager; 10 | import org.asamk.signal.output.JsonWriter; 11 | 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | public interface JsonRpcMultiLocalCommand extends JsonRpcMultiCommand>, MultiLocalCommand { 16 | 17 | default TypeReference> getRequestType() { 18 | return new TypeReference<>() {}; 19 | } 20 | 21 | default void handleCommand( 22 | Map request, 23 | MultiAccountManager c, 24 | JsonWriter jsonWriter 25 | ) throws CommandException { 26 | Namespace commandNamespace = new JsonRpcNamespace(request == null ? Map.of() : request); 27 | handleCommand(commandNamespace, c, jsonWriter); 28 | } 29 | 30 | default List getSupportedOutputTypes() { 31 | return List.of(OutputType.PLAIN_TEXT, OutputType.JSON); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/SendContactsCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Namespace; 4 | import net.sourceforge.argparse4j.inf.Subparser; 5 | 6 | import org.asamk.signal.commands.exceptions.CommandException; 7 | import org.asamk.signal.commands.exceptions.IOErrorException; 8 | import org.asamk.signal.manager.Manager; 9 | import org.asamk.signal.output.OutputWriter; 10 | 11 | import java.io.IOException; 12 | 13 | public class SendContactsCommand implements JsonRpcLocalCommand { 14 | 15 | @Override 16 | public String getName() { 17 | return "sendContacts"; 18 | } 19 | 20 | @Override 21 | public void attachToSubparser(final Subparser subparser) { 22 | subparser.help("Send a synchronization message with the local contacts list to all linked devices."); 23 | } 24 | 25 | @Override 26 | public void handleCommand( 27 | final Namespace ns, 28 | final Manager m, 29 | final OutputWriter outputWriter 30 | ) throws CommandException { 31 | try { 32 | m.sendContacts(); 33 | } catch (IOException e) { 34 | throw new IOErrorException("SendContacts error: " + e.getMessage(), e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | public record SendMessageResults(long timestamp, Map> results) { 7 | 8 | public boolean hasSuccess() { 9 | return results.values() 10 | .stream() 11 | .flatMap(res -> res.stream().map(SendMessageResult::isSuccess)) 12 | .anyMatch(success -> success) || results.values().stream().mapToInt(List::size).sum() == 0; 13 | } 14 | 15 | public boolean hasOnlyUntrustedIdentity() { 16 | return results.values() 17 | .stream() 18 | .flatMap(res -> res.stream().map(SendMessageResult::isIdentityFailure)) 19 | .allMatch(identityFailure -> identityFailure) 20 | && results.values().stream().mapToInt(List::size).sum() > 0; 21 | } 22 | 23 | public boolean hasOnlyRateLimitFailure() { 24 | return results.values() 25 | .stream() 26 | .flatMap(res -> res.stream().map(SendMessageResult::isRateLimitFailure)) 27 | .allMatch(r -> r) && results.values().stream().mapToInt(List::size).sum() > 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/Message.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public record Message( 7 | String messageText, 8 | List attachments, 9 | boolean viewOnce, 10 | List mentions, 11 | Optional quote, 12 | Optional sticker, 13 | List previews, 14 | Optional storyReply, 15 | List textStyles 16 | ) { 17 | 18 | public record Mention(RecipientIdentifier.Single recipient, int start, int length) {} 19 | 20 | public record Quote( 21 | long timestamp, 22 | RecipientIdentifier.Single author, 23 | String message, 24 | List mentions, 25 | List textStyles, 26 | List attachments 27 | ) { 28 | 29 | public record Attachment(String contentType, String filename, String preview) {} 30 | } 31 | 32 | public record Sticker(byte[] packId, int stickerId) {} 33 | 34 | public record Preview(String url, String title, String description, Optional image) {} 35 | 36 | public record StoryReply(long timestamp, RecipientIdentifier.Single author) {} 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager; 2 | 3 | import org.asamk.signal.manager.api.CaptchaRequiredException; 4 | import org.asamk.signal.manager.api.IncorrectPinException; 5 | import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; 6 | import org.asamk.signal.manager.api.PinLockMissingException; 7 | import org.asamk.signal.manager.api.PinLockedException; 8 | import org.asamk.signal.manager.api.RateLimitException; 9 | import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; 10 | 11 | import java.io.Closeable; 12 | import java.io.IOException; 13 | 14 | public interface RegistrationManager extends Closeable { 15 | 16 | void register( 17 | boolean voiceVerification, 18 | String captcha, 19 | final boolean forceRegister 20 | ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException; 21 | 22 | void verifyAccount( 23 | String verificationCode, 24 | String pin 25 | ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException; 26 | 27 | void deleteLocalAccountData() throws IOException; 28 | 29 | boolean isRegistered(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.recipients; 2 | 3 | import java.util.Objects; 4 | 5 | public final class RecipientId { 6 | 7 | private long id; 8 | private final RecipientStore recipientStore; 9 | 10 | RecipientId(long id, final RecipientStore recipientStore) { 11 | this.id = id; 12 | this.recipientStore = recipientStore; 13 | } 14 | 15 | public long id() { 16 | if (recipientStore != null) { 17 | final var actualRecipientId = recipientStore.getActualRecipientId(this.id); 18 | if (actualRecipientId != this.id) { 19 | this.id = actualRecipientId; 20 | } 21 | } 22 | return this.id; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object obj) { 27 | if (obj == this) return true; 28 | if (obj == null || obj.getClass() != this.getClass()) return false; 29 | var that = (RecipientId) obj; 30 | return this.id() == that.id(); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hash(id()); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "RecipientId[" + "id=" + id() + ']'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Namespace; 4 | import net.sourceforge.argparse4j.inf.Subparser; 5 | 6 | import org.asamk.signal.commands.exceptions.CommandException; 7 | import org.asamk.signal.commands.exceptions.IOErrorException; 8 | import org.asamk.signal.manager.Manager; 9 | import org.asamk.signal.output.OutputWriter; 10 | 11 | import java.io.IOException; 12 | 13 | public class SendSyncRequestCommand implements JsonRpcLocalCommand { 14 | 15 | @Override 16 | public String getName() { 17 | return "sendSyncRequest"; 18 | } 19 | 20 | @Override 21 | public void attachToSubparser(final Subparser subparser) { 22 | subparser.help("Send a synchronization request message to primary device (for group, contacts, ...)."); 23 | } 24 | 25 | @Override 26 | public void handleCommand( 27 | final Namespace ns, 28 | final Manager m, 29 | final OutputWriter outputWriter 30 | ) throws CommandException { 31 | try { 32 | m.requestAllSyncData(); 33 | } catch (IOException e) { 34 | throw new IOErrorException("Request sync data error: " + e.getMessage(), e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/commands/JsonRpcNamespace.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.commands; 2 | 3 | import net.sourceforge.argparse4j.inf.Namespace; 4 | 5 | import org.asamk.signal.util.Util; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Namespace implementation, that has plural handling for list arguments and converts camel case keys to dashed strings 12 | */ 13 | final class JsonRpcNamespace extends Namespace { 14 | 15 | public JsonRpcNamespace(final Map attrs) { 16 | super(attrs); 17 | } 18 | 19 | @Override 20 | public T get(String dest) { 21 | final T value = super.get(dest); 22 | if (value != null) { 23 | return value; 24 | } 25 | 26 | final var camelCaseString = Util.dashSeparatedToCamelCaseString(dest); 27 | return super.get(camelCaseString); 28 | } 29 | 30 | @Override 31 | public List getList(final String dest) { 32 | try { 33 | final List value = super.getList(dest); 34 | if (value != null) { 35 | return value; 36 | } 37 | } catch (ClassCastException e) { 38 | return List.of(this.get(dest)); 39 | } 40 | 41 | return super.getList(dest + "s"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/internal/LibSignalLogger.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.internal; 2 | 3 | import org.signal.libsignal.protocol.logging.SignalProtocolLogger; 4 | import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public class LibSignalLogger implements SignalProtocolLogger { 9 | 10 | private static final Logger logger = LoggerFactory.getLogger("LibSignal"); 11 | 12 | public static void initLogger() { 13 | SignalProtocolLoggerProvider.setProvider(new LibSignalLogger()); 14 | } 15 | 16 | private LibSignalLogger() { 17 | } 18 | 19 | @Override 20 | public void log(final int priority, final String tag, final String message) { 21 | final var logMessage = String.format("[%s]: %s", tag, message); 22 | switch (priority) { 23 | case SignalProtocolLogger.VERBOSE -> logger.trace(logMessage); 24 | case SignalProtocolLogger.DEBUG -> logger.debug(logMessage); 25 | case SignalProtocolLogger.INFO -> logger.info(logMessage); 26 | case SignalProtocolLogger.WARN -> logger.warn(logMessage); 27 | case SignalProtocolLogger.ERROR, SignalProtocolLogger.ASSERT -> logger.error(logMessage); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonReceiveMessageHandler.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import org.asamk.signal.manager.Manager; 4 | import org.asamk.signal.manager.api.MessageEnvelope; 5 | import org.asamk.signal.output.JsonWriter; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.HashMap; 10 | 11 | public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { 12 | 13 | private static final Logger logger = LoggerFactory.getLogger(JsonReceiveMessageHandler.class); 14 | 15 | private final Manager m; 16 | private final JsonWriter jsonWriter; 17 | 18 | public JsonReceiveMessageHandler(Manager m, JsonWriter jsonWriter) { 19 | this.m = m; 20 | this.jsonWriter = jsonWriter; 21 | } 22 | 23 | @Override 24 | public void handleMessage(MessageEnvelope envelope, Throwable exception) { 25 | final var object = new HashMap(); 26 | object.put("account", m.getSelfNumber()); 27 | if (exception != null) { 28 | object.put("exception", JsonError.from(exception)); 29 | } 30 | 31 | if (envelope != null) { 32 | object.put("envelope", JsonMessageEnvelope.from(envelope, exception, m)); 33 | } 34 | 35 | jsonWriter.write(object); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/stickers/LegacyStickerStore.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.stickers; 2 | 3 | import org.asamk.signal.manager.api.StickerPackId; 4 | 5 | import java.util.Base64; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | public class LegacyStickerStore { 11 | 12 | public static void migrate(Storage storage, StickerStore stickerStore) { 13 | final var packIds = new HashSet(); 14 | final var stickers = storage.stickers.stream().map(s -> { 15 | var packId = StickerPackId.deserialize(Base64.getDecoder().decode(s.packId)); 16 | if (packIds.contains(packId)) { 17 | // Remove legacy duplicate packIds ... 18 | return null; 19 | } 20 | packIds.add(packId); 21 | var packKey = Base64.getDecoder().decode(s.packKey); 22 | var installed = s.installed; 23 | return new StickerPack(-1, packId, packKey, installed); 24 | }).filter(Objects::nonNull).toList(); 25 | 26 | stickerStore.addLegacyStickers(stickers); 27 | } 28 | 29 | public record Storage(List stickers) { 30 | 31 | public record Sticker(String packId, String packKey, boolean installed) { 32 | 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonContact.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | 5 | import java.util.List; 6 | 7 | public record JsonContact( 8 | String number, 9 | String uuid, 10 | String username, 11 | String name, 12 | String givenName, 13 | String familyName, 14 | String nickName, 15 | String nickGivenName, 16 | String nickFamilyName, 17 | String note, 18 | String color, 19 | boolean isBlocked, 20 | boolean isHidden, 21 | int messageExpirationTime, 22 | boolean profileSharing, 23 | boolean unregistered, 24 | JsonProfile profile, 25 | @JsonInclude(JsonInclude.Include.NON_NULL) JsonInternal internal 26 | ) { 27 | 28 | public record JsonProfile( 29 | long lastUpdateTimestamp, 30 | String givenName, 31 | String familyName, 32 | String about, 33 | String aboutEmoji, 34 | boolean hasAvatar, 35 | String mobileCoinAddress 36 | ) {} 37 | 38 | public record JsonInternal( 39 | List capabilities, 40 | String unidentifiedAccessMode, 41 | Boolean sharesPhoneNumber, 42 | Boolean discoverableByPhonenumber 43 | ) {} 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.api.GroupIdV1; 4 | import org.asamk.signal.manager.helper.Context; 5 | import org.asamk.signal.manager.storage.recipients.RecipientId; 6 | 7 | public class SendGroupInfoAction implements HandleAction { 8 | 9 | private final RecipientId recipientId; 10 | private final GroupIdV1 groupId; 11 | 12 | public SendGroupInfoAction(final RecipientId recipientId, final GroupIdV1 groupId) { 13 | this.recipientId = recipientId; 14 | this.groupId = groupId; 15 | } 16 | 17 | @Override 18 | public void execute(Context context) throws Throwable { 19 | context.getGroupHelper().sendGroupInfoMessage(groupId, recipientId); 20 | } 21 | 22 | @Override 23 | public boolean equals(final Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | 27 | final var that = (SendGroupInfoAction) o; 28 | 29 | if (!recipientId.equals(that.recipientId)) return false; 30 | return groupId.equals(that.groupId); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | var result = recipientId.hashCode(); 36 | result = 31 * result + groupId.hashCode(); 37 | return result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.contacts; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import org.asamk.signal.manager.storage.recipients.RecipientAddress; 7 | import org.signal.core.models.ServiceId.ACI; 8 | 9 | import java.util.UUID; 10 | 11 | import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY; 12 | 13 | public class LegacyContactInfo { 14 | 15 | @JsonProperty 16 | public String name; 17 | 18 | @JsonProperty 19 | public String number; 20 | 21 | @JsonProperty 22 | public UUID uuid; 23 | 24 | @JsonProperty 25 | public String color; 26 | 27 | @JsonProperty(defaultValue = "0") 28 | public int messageExpirationTime; 29 | 30 | @JsonProperty(access = WRITE_ONLY) 31 | public String profileKey; 32 | 33 | @JsonProperty(defaultValue = "false") 34 | public boolean blocked; 35 | 36 | @JsonProperty 37 | public Integer inboxPosition; 38 | 39 | @JsonProperty(defaultValue = "false") 40 | public boolean archived; 41 | 42 | public LegacyContactInfo() { 43 | } 44 | 45 | @JsonIgnore 46 | public RecipientAddress getAddress() { 47 | return new RecipientAddress(uuid == null ? null : ACI.from(uuid), number); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.api.GroupIdV1; 4 | import org.asamk.signal.manager.helper.Context; 5 | import org.asamk.signal.manager.storage.recipients.RecipientId; 6 | 7 | public class SendGroupInfoRequestAction implements HandleAction { 8 | 9 | private final RecipientId recipientId; 10 | private final GroupIdV1 groupId; 11 | 12 | public SendGroupInfoRequestAction(final RecipientId recipientId, final GroupIdV1 groupId) { 13 | this.recipientId = recipientId; 14 | this.groupId = groupId; 15 | } 16 | 17 | @Override 18 | public void execute(Context context) throws Throwable { 19 | context.getGroupHelper().sendGroupInfoRequest(groupId, recipientId); 20 | } 21 | 22 | @Override 23 | public boolean equals(final Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | 27 | final var that = (SendGroupInfoRequestAction) o; 28 | 29 | if (!recipientId.equals(that.recipientId)) return false; 30 | return groupId.equals(that.groupId); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | var result = recipientId.hashCode(); 36 | result = 31 * result + groupId.hashCode(); 37 | return result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 5 | 6 | import org.asamk.signal.manager.Manager; 7 | import org.asamk.signal.manager.api.MessageEnvelope; 8 | import org.asamk.signal.manager.api.RecipientAddress; 9 | 10 | import java.util.UUID; 11 | 12 | record JsonSyncDataMessage( 13 | @Deprecated String destination, 14 | String destinationNumber, 15 | String destinationUuid, 16 | @JsonInclude(JsonInclude.Include.NON_NULL) JsonEditMessage editMessage, 17 | @JsonUnwrapped JsonDataMessage dataMessage 18 | ) { 19 | 20 | static JsonSyncDataMessage from(MessageEnvelope.Sync.Sent transcriptMessage, Manager m) { 21 | return new JsonSyncDataMessage(transcriptMessage.destination() 22 | .map(RecipientAddress::getLegacyIdentifier) 23 | .orElse(null), 24 | transcriptMessage.destination().flatMap(RecipientAddress::number).orElse(null), 25 | transcriptMessage.destination().flatMap(address -> address.uuid().map(UUID::toString)).orElse(null), 26 | transcriptMessage.editMessage().map(data -> JsonEditMessage.from(data, m)).orElse(null), 27 | transcriptMessage.message().map(data -> JsonDataMessage.from(data, m)).orElse(null)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.identities; 2 | 3 | import org.asamk.signal.manager.api.TrustLevel; 4 | import org.signal.core.models.ServiceId; 5 | import org.signal.libsignal.protocol.IdentityKey; 6 | 7 | public class IdentityInfo { 8 | 9 | private final String address; 10 | private final IdentityKey identityKey; 11 | private final TrustLevel trustLevel; 12 | private final long addedTimestamp; 13 | 14 | IdentityInfo(final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp) { 15 | this.address = address; 16 | this.identityKey = identityKey; 17 | this.trustLevel = trustLevel; 18 | this.addedTimestamp = addedTimestamp; 19 | } 20 | 21 | public ServiceId getServiceId() { 22 | return ServiceId.parseOrThrow(address); 23 | } 24 | 25 | public String getAddress() { 26 | return address; 27 | } 28 | 29 | public IdentityKey getIdentityKey() { 30 | return this.identityKey; 31 | } 32 | 33 | public TrustLevel getTrustLevel() { 34 | return this.trustLevel; 35 | } 36 | 37 | boolean isTrusted() { 38 | return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; 39 | } 40 | 41 | public long getDateAddedTimestamp() { 42 | return this.addedTimestamp; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.actions; 2 | 3 | import org.asamk.signal.manager.helper.Context; 4 | import org.asamk.signal.manager.storage.recipients.RecipientId; 5 | import org.signal.core.models.ServiceId; 6 | 7 | public class RenewSessionAction implements HandleAction { 8 | 9 | private final RecipientId recipientId; 10 | private final ServiceId serviceId; 11 | private final ServiceId accountId; 12 | 13 | public RenewSessionAction(final RecipientId recipientId, final ServiceId serviceId, final ServiceId accountId) { 14 | this.recipientId = recipientId; 15 | this.serviceId = serviceId; 16 | this.accountId = accountId; 17 | } 18 | 19 | @Override 20 | public void execute(Context context) throws Throwable { 21 | context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId); 22 | context.getSendHelper().sendNullMessage(recipientId); 23 | } 24 | 25 | @Override 26 | public boolean equals(final Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | 30 | final RenewSessionAction that = (RenewSessionAction) o; 31 | 32 | return recipientId.equals(that.recipientId); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return recipientId.hashCode(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/storage/configuration/LegacyConfigurationStore.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.storage.configuration; 2 | 3 | import org.asamk.signal.manager.api.PhoneNumberSharingMode; 4 | 5 | public class LegacyConfigurationStore { 6 | 7 | public static void migrate(Storage storage, ConfigurationStore configurationStore) { 8 | if (storage.readReceipts != null) { 9 | configurationStore.setReadReceipts(storage.readReceipts); 10 | } 11 | if (storage.unidentifiedDeliveryIndicators != null) { 12 | configurationStore.setUnidentifiedDeliveryIndicators(storage.unidentifiedDeliveryIndicators); 13 | } 14 | if (storage.typingIndicators != null) { 15 | configurationStore.setTypingIndicators(storage.typingIndicators); 16 | } 17 | if (storage.linkPreviews != null) { 18 | configurationStore.setLinkPreviews(storage.linkPreviews); 19 | } 20 | if (storage.phoneNumberSharingMode != null) { 21 | configurationStore.setPhoneNumberSharingMode(storage.phoneNumberSharingMode); 22 | } 23 | } 24 | 25 | public record Storage( 26 | Boolean readReceipts, 27 | Boolean unidentifiedDeliveryIndicators, 28 | Boolean typingIndicators, 29 | Boolean linkPreviews, 30 | Boolean phoneNumberUnlisted, 31 | PhoneNumberSharingMode phoneNumberSharingMode 32 | ) {} 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/main/java/org/asamk/signal/manager/api/ProofRequiredException.java: -------------------------------------------------------------------------------- 1 | package org.asamk.signal.manager.api; 2 | 3 | import java.util.Set; 4 | import java.util.stream.Collectors; 5 | 6 | /** 7 | * Thrown when rate-limited by the server and proof of humanity is required to continue messaging. 8 | */ 9 | public class ProofRequiredException extends Exception { 10 | 11 | private final String token; 12 | private final Set