├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitmodules ├── .nb-gradle-properties ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── misc ├── kon_snap1.png └── kon_snap2.png ├── nbproject ├── licenseheader.txt └── project.properties ├── settings.gradle ├── src ├── main │ ├── java │ │ └── org │ │ │ └── kontalk │ │ │ ├── Kontalk.java │ │ │ ├── client │ │ │ ├── AcknowledgedListener.java │ │ │ ├── AvatarSendReceiver.java │ │ │ ├── BlockListListener.java │ │ │ ├── BlockSendReceiver.java │ │ │ ├── Client.java │ │ │ ├── EndpointServer.java │ │ │ ├── FeatureDiscovery.java │ │ │ ├── HKPClient.java │ │ │ ├── HTTPFileClient.java │ │ │ ├── HTTPFileSlotRequester.java │ │ │ ├── KonConnection.java │ │ │ ├── KonConnectionListener.java │ │ │ ├── KonMessageListener.java │ │ │ ├── KonMessageSender.java │ │ │ ├── KonRosterListener.java │ │ │ ├── LastActivityListener.java │ │ │ ├── PresenceListener.java │ │ │ ├── PrivateKeyReceiver.java │ │ │ ├── PublicKeyListener.java │ │ │ └── VCardListener.java │ │ │ ├── crypto │ │ │ ├── Coder.java │ │ │ ├── Decryptor.java │ │ │ ├── Encryptor.java │ │ │ ├── PGPUtils.java │ │ │ ├── PersonalKey.java │ │ │ └── X509Bridge.java │ │ │ ├── misc │ │ │ ├── Callback.java │ │ │ ├── JID.java │ │ │ ├── KonException.java │ │ │ ├── Searchable.java │ │ │ └── ViewEvent.java │ │ │ ├── model │ │ │ ├── Account.java │ │ │ ├── Avatar.java │ │ │ ├── Contact.java │ │ │ ├── ContactList.java │ │ │ ├── Model.java │ │ │ ├── chat │ │ │ │ ├── Chat.java │ │ │ │ ├── ChatList.java │ │ │ │ ├── ChatMessages.java │ │ │ │ ├── GroupChat.java │ │ │ │ ├── GroupMetaData.java │ │ │ │ ├── Member.java │ │ │ │ ├── ProtoMember.java │ │ │ │ └── SingleChat.java │ │ │ └── message │ │ │ │ ├── CoderStatus.java │ │ │ │ ├── DecryptMessage.java │ │ │ │ ├── InMessage.java │ │ │ │ ├── KonMessage.java │ │ │ │ ├── MessageContent.java │ │ │ │ ├── OutMessage.java │ │ │ │ ├── ProtoMessage.java │ │ │ │ └── Transmission.java │ │ │ ├── persistence │ │ │ ├── Config.java │ │ │ └── Database.java │ │ │ ├── system │ │ │ ├── AccountImporter.java │ │ │ ├── AttachmentManager.java │ │ │ ├── AvatarHandler.java │ │ │ ├── ChatStateManager.java │ │ │ ├── Control.java │ │ │ ├── GroupControl.java │ │ │ └── RosterHandler.java │ │ │ ├── util │ │ │ ├── ClientUtils.java │ │ │ ├── CryptoUtils.java │ │ │ ├── EncodingUtils.java │ │ │ ├── MediaUtils.java │ │ │ ├── MessageUtils.java │ │ │ ├── OggClip.java │ │ │ ├── Tr.java │ │ │ ├── TrustUtils.java │ │ │ └── XMPPUtils.java │ │ │ └── view │ │ │ ├── AvatarLoader.java │ │ │ ├── ChatDetails.java │ │ │ ├── ChatListView.java │ │ │ ├── ChatView.java │ │ │ ├── ComponentUtils.java │ │ │ ├── ComposingArea.java │ │ │ ├── ConfigurationDialog.java │ │ │ ├── ContactDetails.java │ │ │ ├── ContactListView.java │ │ │ ├── Content.java │ │ │ ├── ImageLoader.java │ │ │ ├── ImportDialog.java │ │ │ ├── LinkUtils.java │ │ │ ├── ListView.java │ │ │ ├── MainFrame.java │ │ │ ├── MessageList.java │ │ │ ├── Notifier.java │ │ │ ├── ObserverTrait.java │ │ │ ├── ProfileDialog.java │ │ │ ├── SearchPanel.java │ │ │ ├── TrayManager.java │ │ │ ├── Utils.java │ │ │ └── View.java │ └── resources │ │ ├── i18n │ │ ├── strings.properties │ │ ├── strings_ca.properties │ │ ├── strings_cs.properties │ │ ├── strings_de.properties │ │ ├── strings_el.properties │ │ ├── strings_es.properties │ │ ├── strings_eu.properties │ │ ├── strings_fa.properties │ │ ├── strings_fi.properties │ │ ├── strings_fr.properties │ │ ├── strings_in.properties │ │ ├── strings_it.properties │ │ ├── strings_ja.properties │ │ ├── strings_lt.properties │ │ ├── strings_nb_no.properties │ │ ├── strings_nl.properties │ │ ├── strings_ru.properties │ │ ├── strings_sr.properties │ │ └── strings_zh.properties │ │ ├── img │ │ ├── chat_bg.png │ │ ├── ic_msg_crypt.png │ │ ├── ic_msg_crypt_warning.png │ │ ├── ic_msg_delivered.png │ │ ├── ic_msg_error.png │ │ ├── ic_msg_pending.png │ │ ├── ic_msg_sent.png │ │ ├── ic_msg_unencrypt.png │ │ ├── ic_msg_warning.png │ │ ├── ic_ui_add_contact.png │ │ ├── ic_ui_add_group.png │ │ ├── ic_ui_attach.png │ │ ├── ic_ui_clear.png │ │ ├── ic_ui_edit.png │ │ ├── ic_ui_insecure.png │ │ ├── ic_ui_menu.png │ │ ├── ic_ui_refresh.png │ │ ├── ic_ui_reload.png │ │ ├── ic_ui_search.png │ │ ├── ic_ui_secure.png │ │ ├── ic_ui_send.png │ │ ├── ic_ui_warning.png │ │ ├── kontalk-big.png │ │ ├── kontalk.png │ │ ├── kontalk_notification.png │ │ └── kontalk_small.png │ │ ├── notification.ogg │ │ └── truststore.bks └── test │ └── java │ └── org │ └── kontalk │ ├── KontalkTest.java │ └── util │ └── CryptoUtilsTest.java ├── update_tr.py └── win_installer ├── create_installer.nsi └── kontalk.ico /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | (Template: Write your error report, question or wish here. Please be as precise as possible.) 3 | 4 | ### Additional information 5 | Kontalk Desktop version: 6 | Operating System: 7 | Desktop Environment: (if using Linux) 8 | Kontalk Android version: (if Android device is involved) 9 | 10 | (For errors please check for exceptions or warnings in the log file and 11 | include them here. More information: https://github.com/kontalk/desktopclient-java/wiki#i-found-a-bug-what-to-do ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # EDIT: 4 | /nbproject/private/ 5 | /kontalk.db 6 | /kontalk.properties 7 | *.pgp 8 | *.asc 9 | *.crt 10 | /win_installer/*.exe 11 | 12 | # Gradle 13 | /.gradle/ 14 | 15 | # Gradle Netbeans plugin 16 | /.nb-gradle/ 17 | 18 | # IntelliJ Idea 19 | .idea/ 20 | *.iml 21 | out/ 22 | 23 | # Package Files # 24 | #*.jar 25 | *.war 26 | *.ear 27 | 28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 29 | hs_err_pid* 30 | build/ 31 | dist/ 32 | store/ 33 | release/ 34 | 35 | /.classpath 36 | /.project 37 | /.settings 38 | /bin/ 39 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "client-common-java"] 2 | path = client-common-java 3 | url = https://github.com/kontalk/client-common-java.git 4 | -------------------------------------------------------------------------------- /.nb-gradle-properties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GPL 6 | 7 | Kontalk 8 | 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | before_install: 7 | - git submodule update --init 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | kontalk-java-client 2 | =================== 3 | 4 | [![Build Status](https://travis-ci.org/kontalk/desktopclient-java.svg?branch=master)](https://travis-ci.org/kontalk/desktopclient-java) 5 | 6 | A platform independent Java client for Kontalk (http://www.kontalk.org). Includes connectivity to the Jabber network! 7 | 8 | The desktop client uses your existing Kontalk account from the [Android client](https://github.com/kontalk/androidclient/blob/master/README.md#kontalk-official-android-client). Instructions for exporting the key [here](https://github.com/kontalk/androidclient/wiki/Export-personal-key-to-another-device). 9 | 10 | **FAQ:** Common questions are answered [here!](https://github.com/kontalk/desktopclient-java/wiki/FAQ) 11 | 12 | User Forum: https://forum.kontalk.org/ 13 | 14 | ### Screenshots 15 | 16 | ![Conversation screen](/misc/kon_snap1.png?raw=true) 17 | 18 | [Contacts screen](/misc/kon_snap2.png?raw=true) 19 | 20 | ## Software Dependencies 21 | 22 | - Java 8 (Java Runtime Engine) 23 | 24 | ## Key Features 25 | 26 | Connect with Kontalk... 27 | - Use the existing Kontalk account from your phone 28 | - Synchronized contact list (=XMPP roster) 29 | - Add new Kontalk users by phone number 30 | - The client automatically requests public keys for safe communication 31 | - Your communication with Kontalk users is encrypted by default 32 | 33 | ... and beyond: 34 | - Exchange text messages with any Jabber/XMPP users! 35 | - Add new Jabber contacts by JID 36 | - Tested with clients like [Pidgin](https://pidgin.im/) or [Conversations](https://github.com/siacs/Conversations) 37 | 38 | **Note: private key and messages are saved unencrypted and can be read by other applications on your computer!** 39 | 40 | ## Implemented XEP features: 41 | - XEP-0184: Message receipts 42 | - XEP-0085: Chat state notifications 43 | - XEP-0191: User blocking 44 | - XEP-0066: File transfer over server 45 | - XEP-0231: Image thumbnails for attachments 46 | - XEP-0084: Avatar images 47 | - XEP-0363: HTTP File Upload 48 | - XEP-0012: Last activity timestamp 49 | - XEP-0245: The infamous and most essential "/me" command 50 | 51 | ## Support us 52 | 53 | * If you are missing a feature or found a bug [report it!](https://github.com/kontalk/desktopclient-java/issues) 54 | 55 | * Help us with [translations](https://translate.kontalk.org) to spread Kontalk around the world! 56 | 57 | * Code contributions / pull requests are welcome! 58 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | apply plugin: 'application' 3 | apply plugin: 'java' 4 | 5 | sourceCompatibility = '1.8' 6 | targetCompatibility = '1.8' 7 | 8 | mainClassName = 'org.kontalk.Kontalk' 9 | ext.clientCommonDir = 'client-common-java' 10 | 11 | applicationDefaultJvmArgs = ['''-Djava.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS|%4$-6s|%2$s-%5$s%6$s%n'''] 12 | 13 | if (!file('./'+clientCommonDir+'/build.gradle').exists()) { 14 | throw new GradleException('Submodule not found. Run "git submodule update --init".') 15 | } 16 | 17 | gradle.projectsEvaluated { 18 | tasks.withType(JavaCompile) { 19 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 20 | } 21 | } 22 | 23 | evaluationDependsOn ':'+clientCommonDir 24 | 25 | ext.bcVersion = '1.60' 26 | ext.smackVersion = project(':'+clientCommonDir).smackVersion 27 | 28 | //configurations.all { transitive = false } 29 | 30 | dependencies { 31 | compile project(':'+clientCommonDir) 32 | 33 | compile group: 'org.bouncycastle', name: 'bcpg-jdk15on', version: "$bcVersion" 34 | compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: "$bcVersion" 35 | compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: "$bcVersion" 36 | compile group: 'commons-cli', name: 'commons-cli', version: "1.3.1" 37 | compile group: 'commons-codec', name: 'commons-codec', version: "1.10" 38 | compile group: 'commons-configuration', name: 'commons-configuration', version: "1.10" 39 | compile group: 'commons-io', name: 'commons-io', version: "2.4" 40 | compile group: 'commons-lang', name: 'commons-lang', version: "2.6" 41 | compile group: 'commons-logging', name: 'commons-logging', version: "1.1.3" 42 | compile group: 'org.apache.httpcomponents', name: 'httpclient', version: "4.5.6" 43 | compile group: 'org.apache.httpcomponents', name: 'httpcore', version: "4.4.10" 44 | compile group: 'org.apache.tika', name: 'tika-core', version: "1.18" 45 | compile group: 'org.jcraft', name: 'jorbis', version: "0.0.17" 46 | //compile group: 'org.jxmpp', name: 'jxmpp-core', version: "0.4.2" 47 | //compile group: 'org.jxmpp', name: 'jxmpp-util-cache', version: "0.4.2" 48 | compile group: 'org.jxmpp', name: 'jxmpp-jid', version: "0.4.2" 49 | compile group: 'net.sf.kxml', name: 'kxml2', version: "2.3.0" 50 | // missing for junit in json-simple's pom 51 | compile group: 'com.googlecode.json-simple', name: 'json-simple', version: "1.1.1", transitive: false 52 | compile group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: "7.0.8" 53 | compile group: 'org.ocpsoft.prettytime', name: 'prettytime', version: "3.2.7.Final" 54 | compile group: 'org.igniterealtime.smack', name: 'smack-core', version: "$smackVersion" 55 | compile group: 'org.igniterealtime.smack', name: 'smack-extensions', version: "$smackVersion" 56 | compile group: 'org.igniterealtime.smack', name: 'smack-im', version: "$smackVersion" 57 | compile group: 'org.igniterealtime.smack', name: 'smack-java7', version: "$smackVersion" 58 | compile group: 'org.igniterealtime.smack', name: 'smack-tcp', version: "$smackVersion" 59 | compile group: 'org.xerial', name: 'sqlite-jdbc', version: "3.23.1" 60 | compile group: 'de.sciss', name: 'weblaf', version: "1.28" 61 | 62 | testCompile group: 'junit', name: 'junit', version: "4.12" 63 | } 64 | 65 | repositories { 66 | mavenCentral() 67 | } 68 | 69 | run { 70 | if (project.hasProperty('jvmargs')) { 71 | jvmArgs(jvmargs.split(',')) 72 | } 73 | } 74 | 75 | task deleteDeps(type: Delete) { 76 | delete fileTree(project.file('dist/lib')) { 77 | include '*.jar' 78 | } 79 | } 80 | 81 | task copyDeps(type: Copy) { 82 | from(configurations.runtime) 83 | into project.file('dist/lib') 84 | dependsOn ':deleteDeps' 85 | } 86 | 87 | jar { 88 | baseName = 'KontalkDesktopApp' 89 | destinationDir project.file('dist') 90 | 91 | manifest { 92 | attributes( 93 | "Main-Class": mainClassName, 94 | "Class-Path": configurations.compile.collect { 'lib/'+it.getName() }.join(' ') 95 | ) 96 | } 97 | 98 | dependsOn ':copyDeps' 99 | } 100 | 101 | buildscript { 102 | repositories { 103 | mavenCentral() 104 | } 105 | dependencies { 106 | classpath 'org.owasp:dependency-check-gradle:3.3.1' 107 | } 108 | } 109 | 110 | apply plugin: 'org.owasp.dependencycheck' 111 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 20 17:46:17 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /misc/kon_snap1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/misc/kon_snap1.png -------------------------------------------------------------------------------- /misc/kon_snap2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/misc/kon_snap2.png -------------------------------------------------------------------------------- /nbproject/licenseheader.txt: -------------------------------------------------------------------------------- 1 | <#if licenseFirst??> 2 | ${licenseFirst} 3 | 4 | ${licensePrefix} Kontalk Java client 5 | ${licensePrefix} Copyright (C) 2016 Kontalk Devteam 6 | ${licensePrefix} 7 | ${licensePrefix} This program is free software: you can redistribute it and/or modify 8 | ${licensePrefix} it under the terms of the GNU General Public License as published by 9 | ${licensePrefix} the Free Software Foundation, either version 3 of the License, or 10 | ${licensePrefix} (at your option) any later version. 11 | ${licensePrefix} 12 | ${licensePrefix} This program is distributed in the hope that it will be useful, 13 | ${licensePrefix} but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ${licensePrefix} MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ${licensePrefix} GNU General Public License for more details. 16 | ${licensePrefix} 17 | ${licensePrefix} You should have received a copy of the GNU General Public License 18 | ${licensePrefix} along with this program. If not, see . 19 | <#if licenseLast??> 20 | ${licenseLast} 21 | -------------------------------------------------------------------------------- /nbproject/project.properties: -------------------------------------------------------------------------------- 1 | build.generated.sources.dir=${build.dir}/generated-sources 2 | excludes= 3 | includes=** 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kontalk_java' 2 | 3 | include 'client-common-java' 4 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/AcknowledgedListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import org.jivesoftware.smack.StanzaListener; 24 | import org.jivesoftware.smack.packet.Message; 25 | import org.jivesoftware.smack.packet.Stanza; 26 | import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; 27 | import org.jivesoftware.smackx.receipts.DeliveryReceipt; 28 | import org.kontalk.system.Control; 29 | import org.kontalk.util.ClientUtils.MessageIDs; 30 | 31 | /** 32 | * Listener for acknowledged packets (Stream Management, XEP-0198). 33 | * @author Alexander Bikadorov {@literal } 34 | */ 35 | final class AcknowledgedListener implements StanzaListener { 36 | private static final Logger LOGGER = Logger.getLogger(AcknowledgedListener.class.getName()); 37 | 38 | private final Control mControl; 39 | 40 | public AcknowledgedListener(Control control) { 41 | mControl = control; 42 | } 43 | 44 | @Override 45 | public void processStanza(Stanza p) { 46 | // NOTE: the packet is not the acknowledgement itself but the packet 47 | // that is acknowledged 48 | if (!(p instanceof Message)) { 49 | // we are only interested in acks for messages 50 | return; 51 | } 52 | Message m = (Message) p; 53 | 54 | LOGGER.config("for message: "+m); 55 | 56 | if (DeliveryReceipt.from(m) != null) { 57 | // this is an ack for a 'received' message (XEP-0184) send by 58 | // KonMessageListener, ignore 59 | return; 60 | } 61 | 62 | if (m.getBody() == null && m.getExtensions().size() == 1 && 63 | m.getExtension(ChatStateExtension.NAMESPACE) != null) { 64 | // this is an ack for a chat state notification (XEP-0085), ignore 65 | return; 66 | } 67 | 68 | mControl.onMessageSent(MessageIDs.to(m)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/BlockListListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.logging.Logger; 24 | import org.jivesoftware.smack.StanzaListener; 25 | import org.jivesoftware.smack.packet.Stanza; 26 | import org.jivesoftware.smack.provider.ProviderManager; 27 | import org.kontalk.misc.JID; 28 | import org.kontalk.system.Control; 29 | 30 | /** 31 | * 32 | * @author Alexander Bikadorov {@literal } 33 | */ 34 | final class BlockListListener implements StanzaListener { 35 | private static final Logger LOGGER = Logger.getLogger(BlockListListener.class.getName()); 36 | 37 | private final Control mControl; 38 | 39 | static { 40 | ProviderManager.addIQProvider(BlockingCommand.BLOCKLIST, 41 | BlockingCommand.NAMESPACE, 42 | new BlockingCommand.Provider()); 43 | } 44 | 45 | BlockListListener(Control control) { 46 | mControl = control; 47 | } 48 | 49 | @Override 50 | public void processStanza(Stanza packet) { 51 | BlockingCommand p = (BlockingCommand) packet; 52 | LOGGER.config("blocking command: "+p); 53 | 54 | List items = p.getItems(); 55 | if (items != null) { 56 | List jids = new ArrayList<>(items.size()); 57 | for (String s : items) 58 | jids.add(JID.full(s)); 59 | mControl.onBlockList(jids.toArray(new JID[0])); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/BlockSendReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import org.jivesoftware.smack.packet.IQ; 24 | import org.jivesoftware.smack.util.SuccessCallback; 25 | import org.kontalk.misc.JID; 26 | import org.kontalk.system.Control; 27 | 28 | /** 29 | * Send blocking command and listen to response. 30 | * @author Alexander Bikadorov {@literal } 31 | */ 32 | final class BlockSendReceiver implements SuccessCallback { 33 | private static final Logger LOGGER = Logger.getLogger(BlockSendReceiver.class.getName()); 34 | 35 | private final Control mControl; 36 | private final KonConnection mConn; 37 | private final boolean mBlocking; 38 | private final JID mJID; 39 | 40 | BlockSendReceiver(Control control, 41 | KonConnection conn, 42 | boolean blocking, 43 | JID jid){ 44 | mControl = control; 45 | mConn = conn; 46 | mBlocking = blocking; 47 | mJID = jid; 48 | } 49 | 50 | void sendAndListen() { 51 | LOGGER.info("jid: "+mJID+" blocking="+mBlocking); 52 | 53 | String command = mBlocking ? BlockingCommand.BLOCK : BlockingCommand.UNBLOCK; 54 | BlockingCommand blockingCommand = new BlockingCommand(command, mJID.string()); 55 | 56 | mConn.sendWithCallback(blockingCommand, this); 57 | } 58 | 59 | 60 | @Override 61 | public void onSuccess(IQ packet) { 62 | LOGGER.info("response: "+packet); 63 | 64 | IQ p = (IQ) packet; 65 | 66 | if (p.getType() != IQ.Type.result) { 67 | LOGGER.warning("ignoring response with IQ type: "+p.getType()); 68 | return; 69 | } 70 | 71 | mControl.onContactBlocked(mJID, mBlocking); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/EndpointServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | /** 22 | * Defines a server address and features. 23 | * @author Daniele Ricci 24 | * @version 1.0 25 | */ 26 | public final class EndpointServer { 27 | 28 | private final String mHost; 29 | private final int mPort; 30 | private final String mNetwork; 31 | 32 | public EndpointServer(String host, int port) { 33 | // tigase: use hostname as network 34 | mNetwork = host; 35 | mHost = host; 36 | mPort = port; 37 | } 38 | 39 | // TODO unused 40 | public EndpointServer(String network, String host, int port) { 41 | mNetwork = network; 42 | mHost = host; 43 | mPort = port; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return mNetwork + "|" + mHost + ":" + mPort; 49 | } 50 | 51 | public String getHost() { 52 | return mHost; 53 | } 54 | 55 | public int getPort() { 56 | return mPort; 57 | } 58 | 59 | public String getNetwork() { 60 | return mNetwork; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/HKPClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.io.IOException; 22 | import java.util.logging.Level; 23 | import java.util.logging.Logger; 24 | import org.apache.commons.io.IOUtils; 25 | import org.apache.http.HttpEntity; 26 | import org.apache.http.HttpStatus; 27 | import org.apache.http.client.methods.CloseableHttpResponse; 28 | import org.apache.http.client.methods.HttpGet; 29 | import org.apache.http.client.methods.HttpRequestBase; 30 | import org.apache.http.impl.client.CloseableHttpClient; 31 | import org.apache.http.impl.client.HttpClients; 32 | 33 | /** 34 | * Client for OpenPGP HTTP Keyserver Protocol. 35 | * 36 | * See https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 37 | * 38 | * @author Alexander Bikadorov {@literal } 39 | */ 40 | public final class HKPClient { 41 | private static final Logger LOGGER = Logger.getLogger(HKPClient.class.getName()); 42 | 43 | //private static final int DEFAULT_PORT = 11371; 44 | //private static final int DEFAULT_SSL_PORT = 443; 45 | 46 | private static final int MAX_CONTENT_LENGTH = 9001; 47 | 48 | public String search(String server, String keyID) { 49 | CloseableHttpClient client = HttpClients.createDefault(); 50 | HttpRequestBase get = new HttpGet( 51 | "https://"+server+"/pks/lookup?op=get&options=mr&exact=on&search=0x"+keyID); 52 | 53 | // execute request 54 | CloseableHttpResponse response; 55 | try { 56 | response = client.execute(get); 57 | } catch (IOException ex) { 58 | LOGGER.log(Level.WARNING, "can't execute request, server: "+server, ex); 59 | return ""; 60 | } 61 | try { 62 | int code = response.getStatusLine().getStatusCode(); 63 | if (code != HttpStatus.SC_OK) { 64 | if (code == HttpStatus.SC_NOT_FOUND) { 65 | LOGGER.config("key not found, server: "+server+"; keyID="+keyID); 66 | } else { 67 | LOGGER.warning("unexpected response, server: "+server+"; code=" + code); 68 | } 69 | return ""; 70 | } 71 | 72 | HttpEntity entity = response.getEntity(); 73 | if (entity == null) { 74 | LOGGER.warning("no download response entity"); 75 | return ""; 76 | } 77 | 78 | if (entity.getContentLength() > MAX_CONTENT_LENGTH) { 79 | LOGGER.warning("content too big"); 80 | return ""; 81 | } 82 | 83 | String contentStr; 84 | try { 85 | contentStr = IOUtils.toString(entity.getContent(), "UTF-8"); 86 | } catch (IOException | IllegalStateException ex) { 87 | LOGGER.log(Level.WARNING, " can't read content", ex); 88 | return ""; 89 | } 90 | 91 | // for (Header h: response.getAllHeaders()) { 92 | // System.out.println("header: "+h); 93 | // } 94 | 95 | return contentStr; 96 | } finally { 97 | try { 98 | response.close(); 99 | } catch (IOException ex) { 100 | LOGGER.log(Level.WARNING, "can't close response", ex); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/HTTPFileSlotRequester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import org.jivesoftware.smack.provider.ProviderManager; 24 | import org.kontalk.misc.Callback; 25 | import org.kontalk.misc.JID; 26 | import org.kontalk.system.AttachmentManager; 27 | import org.kontalk.util.EncodingUtils; 28 | 29 | /** 30 | * 31 | * @author Alexander Bikadorov {@literal } 32 | */ 33 | final class HTTPFileSlotRequester { 34 | private static final Logger LOGGER = Logger.getLogger(HTTPFileSlotRequester.class.getName()); 35 | 36 | static { 37 | ProviderManager.addIQProvider( 38 | HTTPFileUpload.Slot.ELEMENT_NAME, 39 | HTTPFileUpload.NAMESPACE, 40 | new HTTPFileUpload.Slot.Provider()); 41 | } 42 | 43 | private final KonConnection mConn; 44 | private final JID mService; 45 | 46 | public HTTPFileSlotRequester(KonConnection conn, JID service) { 47 | mConn = conn; 48 | mService = service; 49 | } 50 | 51 | private HTTPFileUpload.Slot mSlotPacket; 52 | synchronized AttachmentManager.Slot getSlot(String filename, long size, String mime) { 53 | HTTPFileUpload.Request request = new HTTPFileUpload.Request(filename, size, mime); 54 | request.setTo(mService.toBareSmack()); 55 | 56 | final Callback.Synchronizer syncer = new Callback.Synchronizer(); 57 | mSlotPacket = null; 58 | mConn.sendWithCallback(request, packet -> { 59 | LOGGER.config("response: " + packet); 60 | 61 | if (!(packet instanceof HTTPFileUpload.Slot)) { 62 | LOGGER.warning("response not a slot packet: " + packet); 63 | syncer.sync(); 64 | return; 65 | } 66 | mSlotPacket = (HTTPFileUpload.Slot) packet; 67 | syncer.sync(); 68 | }); 69 | 70 | syncer.waitForSync(); 71 | 72 | return mSlotPacket != null ? 73 | new AttachmentManager.Slot( 74 | EncodingUtils.toURI(mSlotPacket.getPutUrl()), 75 | EncodingUtils.toURI(mSlotPacket.getGetUrl())) : 76 | new AttachmentManager.Slot(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/KonConnectionListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.logging.Level; 22 | import java.util.logging.Logger; 23 | import org.jivesoftware.smack.ConnectionListener; 24 | import org.jivesoftware.smack.XMPPConnection; 25 | import org.kontalk.misc.JID; 26 | import org.kontalk.misc.KonException; 27 | import org.kontalk.system.Control; 28 | 29 | /** 30 | * 31 | * @author Alexander Bikadorov {@literal } 32 | */ 33 | final class KonConnectionListener implements ConnectionListener { 34 | private static final Logger LOGGER = Logger.getLogger(KonConnectionListener.class.getName()); 35 | 36 | private final Client mClient; 37 | private final Control mControl; 38 | 39 | private boolean mConnected = false; 40 | 41 | KonConnectionListener(Client client, Control control) { 42 | mClient = client; 43 | mControl = control; 44 | } 45 | 46 | @Override 47 | public void connected(XMPPConnection connection) { 48 | LOGGER.info("to "+connection.getHost()); 49 | mConnected = true; 50 | } 51 | 52 | @Override 53 | public void authenticated(XMPPConnection connection, boolean resumed) { 54 | JID jid = JID.fromSmack(connection.getUser()); 55 | LOGGER.info("as "+jid); 56 | mControl.onAuthenticated(jid); 57 | } 58 | 59 | @Override 60 | public void connectionClosed() { 61 | mConnected = false; 62 | LOGGER.info("connection closed"); 63 | mClient.newStatus(Control.Status.DISCONNECTED); 64 | } 65 | 66 | @Override 67 | public void connectionClosedOnError(Exception ex) { 68 | // ignore if error occurs on connection attempt (handled by client) 69 | boolean connected = mConnected; 70 | mConnected = false; 71 | 72 | if (!connected) 73 | return; 74 | 75 | LOGGER.log(Level.WARNING, "connection closed on error", ex); 76 | mClient.newStatus(Control.Status.ERROR); 77 | mClient.newException(new KonException(KonException.Error.CLIENT_ERROR, ex)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/KonRosterListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.Collection; 22 | import java.util.Set; 23 | import java.util.logging.Level; 24 | import java.util.logging.Logger; 25 | import java.util.stream.Collectors; 26 | 27 | import org.apache.commons.lang.StringUtils; 28 | import org.jivesoftware.smack.packet.Presence; 29 | import org.jivesoftware.smack.roster.Roster; 30 | import org.jivesoftware.smack.roster.RosterEntry; 31 | import org.jivesoftware.smack.roster.RosterListener; 32 | import org.jivesoftware.smack.roster.RosterLoadedListener; 33 | import org.jxmpp.jid.Jid; 34 | import org.kontalk.misc.JID; 35 | import org.kontalk.system.RosterHandler; 36 | import org.kontalk.util.ClientUtils; 37 | 38 | /** 39 | * Listener for events in the roster (a server-side contact list in XMPP). 40 | * @author Alexander Bikadorov {@literal } 41 | */ 42 | final class KonRosterListener implements RosterLoadedListener, RosterListener { 43 | private static final Logger LOGGER = Logger.getLogger(KonRosterListener.class.getName()); 44 | 45 | private final Roster mRoster; 46 | private final RosterHandler mHandler; 47 | private boolean mLoaded = false; 48 | 49 | KonRosterListener(Roster roster, RosterHandler handler) { 50 | mRoster = roster; 51 | mHandler = handler; 52 | } 53 | 54 | @Override 55 | public void onRosterLoaded(Roster roster) { 56 | Set entries = roster.getEntries(); 57 | LOGGER.info("loading "+entries.size()+" entries"); 58 | 59 | mHandler.onLoaded(entries.stream() 60 | .map(KonRosterListener::clientToModel) 61 | .collect(Collectors.toList())); 62 | mLoaded = true; 63 | } 64 | 65 | @Override 66 | public void onRosterLoadingFailed(Exception exception) { 67 | LOGGER.log(Level.WARNING, "roster loading failed", exception); 68 | } 69 | 70 | /** 71 | * NOTE: on every (re-)connect all entries are added again (loaded), 72 | * one method call for all contacts. 73 | */ 74 | @Override 75 | public void entriesAdded(Collection addresses) { 76 | if (mRoster == null || !mLoaded) 77 | return; 78 | 79 | for (Jid jid: addresses) { 80 | RosterEntry entry = mRoster.getEntry(jid.asBareJid()); 81 | if (entry == null) { 82 | LOGGER.warning("jid not in roster: "+jid); 83 | return; 84 | } 85 | 86 | LOGGER.config("entry: "+entry.toString()); 87 | mHandler.onEntryAdded(clientToModel(entry)); 88 | } 89 | } 90 | 91 | @Override 92 | public void entriesUpdated(Collection addresses) { 93 | // note: we don't know what exactly changed here 94 | for (Jid jid: addresses) { 95 | RosterEntry entry = mRoster.getEntry(jid.asBareJid()); 96 | if (entry == null) { 97 | LOGGER.warning("jid not in roster: "+jid); 98 | return; 99 | } 100 | 101 | LOGGER.info("entry: "+entry.toString()); 102 | mHandler.onEntryUpdate(clientToModel(entry)); 103 | } 104 | } 105 | 106 | @Override 107 | public void entriesDeleted(Collection addresses) { 108 | for (Jid jid: addresses) { 109 | LOGGER.info("address: "+jid); 110 | 111 | mHandler.onEntryDeleted(JID.fromSmack(jid)); 112 | } 113 | } 114 | 115 | @Override 116 | public void presenceChanged(Presence presence) { 117 | // handled by PresenceListener 118 | } 119 | 120 | private static ClientUtils.KonRosterEntry clientToModel(RosterEntry entry) { 121 | return new ClientUtils.KonRosterEntry(JID.fromSmack(entry.getJid()), 122 | StringUtils.defaultString(entry.getName()), 123 | entry.getType(), 124 | entry.isSubscriptionPending()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/LastActivityListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import org.apache.commons.lang.StringUtils; 24 | import org.jivesoftware.smack.SmackException; 25 | import org.jivesoftware.smack.StanzaListener; 26 | import org.jivesoftware.smack.packet.Stanza; 27 | import org.jivesoftware.smackx.iqlast.packet.LastActivity; 28 | import org.kontalk.misc.JID; 29 | import org.kontalk.system.Control; 30 | 31 | /** 32 | * Listener for Last Activity Response packets (XEP-0012). 33 | * @author Alexander Bikadorov {@literal } 34 | */ 35 | class LastActivityListener implements StanzaListener { 36 | private static final Logger LOGGER = Logger.getLogger(LastActivityListener.class.getName()); 37 | 38 | private final Control mControl; 39 | 40 | public LastActivityListener(Control control) { 41 | mControl = control; 42 | } 43 | 44 | @Override 45 | public void processStanza(Stanza packet) throws SmackException.NotConnectedException { 46 | LOGGER.config("last activity: " + packet); 47 | 48 | LastActivity lastActivity = (LastActivity) packet; 49 | 50 | long lastActivityTime = lastActivity.getIdleTime(); 51 | if (lastActivityTime < 0) { 52 | // error message or parsing error, not important here (logged by Client class) 53 | return; 54 | } 55 | 56 | mControl.onLastActivity(JID.fromSmack(lastActivity.getFrom()), 57 | lastActivity.getIdleTime(), 58 | StringUtils.defaultString(lastActivity.getStatusMessage())); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/PresenceListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.Optional; 22 | import java.util.logging.Logger; 23 | 24 | import org.apache.commons.lang.StringUtils; 25 | import org.jivesoftware.smack.StanzaListener; 26 | import org.jivesoftware.smack.packet.ExtensionElement; 27 | import org.jivesoftware.smack.packet.Presence; 28 | import org.jivesoftware.smack.packet.Stanza; 29 | import org.jivesoftware.smack.packet.StanzaError; 30 | import org.jivesoftware.smack.provider.ProviderManager; 31 | import org.jivesoftware.smack.roster.Roster; 32 | import org.jivesoftware.smackx.muc.packet.MUCUser; 33 | import org.kontalk.misc.JID; 34 | import org.kontalk.system.RosterHandler; 35 | 36 | /** 37 | * Listen for presence packets. 38 | * 39 | * The presence stanza also may include a public key fingerprint 40 | * extension (custom Kontalk extension, based on XEP-0189) and/or a signature 41 | * extension for signing the status element (XEP-0027). 42 | * 43 | * @author Alexander Bikadorov {@literal } 44 | */ 45 | class PresenceListener implements StanzaListener { 46 | private static final Logger LOGGER = Logger.getLogger(PresenceListener.class.getName()); 47 | 48 | private final Roster mRoster; 49 | private final RosterHandler mHandler; 50 | 51 | public PresenceListener(Roster roster, RosterHandler handler) { 52 | mRoster = roster; 53 | mHandler = handler; 54 | 55 | ProviderManager.addExtensionProvider( 56 | PublicKeyPresence.ELEMENT_NAME, 57 | PublicKeyPresence.NAMESPACE, 58 | new PublicKeyPresence.Provider()); 59 | 60 | ProviderManager.addExtensionProvider( 61 | PresenceSignature.ELEMENT_NAME, 62 | PresenceSignature.NAMESPACE, 63 | new PresenceSignature.Provider()); 64 | } 65 | 66 | @Override 67 | public void processStanza(Stanza packet) { 68 | if (MUCUser.from(packet) != null) { 69 | // handled by MUC manager 70 | LOGGER.config("ignoring MUC presence, from: "+packet.getFrom()); 71 | return; 72 | } 73 | 74 | LOGGER.config("packet: "+packet); 75 | 76 | Presence presence = (Presence) packet; 77 | 78 | JID jid = JID.fromSmack(presence.getFrom()); 79 | 80 | ExtensionElement publicKeyExt = presence.getExtension( 81 | PublicKeyPresence.ELEMENT_NAME, 82 | PublicKeyPresence.NAMESPACE); 83 | PublicKeyPresence pubKey = publicKeyExt instanceof PublicKeyPresence ? 84 | (PublicKeyPresence) publicKeyExt : 85 | null; 86 | 87 | switch(presence.getType()) { 88 | case error: 89 | StanzaError error = presence.getError(); 90 | if (error == null) { 91 | LOGGER.warning("error presence does not contain error"); 92 | return; 93 | } 94 | mHandler.onPresenceError(jid, error.getType(), 95 | error.getCondition()); 96 | return; 97 | // NOTE: only handled here if Roster.SubscriptionMode is set to 'manual' 98 | case subscribe: 99 | byte[] key = pubKey != null ? pubKey.getKey() : null; 100 | if (key == null) 101 | key = new byte[0]; 102 | mHandler.onSubscriptionRequest(jid, key); 103 | return; 104 | case unsubscribe: 105 | // nothing to do(?) 106 | LOGGER.info(("ignoring unsubscribe, JID: "+jid)); 107 | return; 108 | } 109 | 110 | // NOTE: a delay extension is sometimes included, don't know why; 111 | // ignoring mode, always null anyway 112 | 113 | // NOTE: using only the "best" presence to ignore unimportant updates 114 | // from multiple clients 115 | Presence bestPresence = mRoster.getPresence(jid.toBareSmack()); 116 | 117 | mHandler.onPresenceUpdate(jid, 118 | bestPresence.getType(), 119 | Optional.ofNullable(bestPresence.getStatus())); 120 | 121 | if (pubKey != null) { 122 | String fp = StringUtils.defaultString(pubKey.getFingerprint()).toLowerCase(); 123 | if (fp.isEmpty()) { 124 | LOGGER.warning("no fingerprint in public key presence extension"); 125 | } else { 126 | mHandler.onFingerprintPresence(jid, fp); 127 | } 128 | } 129 | 130 | ExtensionElement signatureExt = bestPresence.getExtension( 131 | PresenceSignature.ELEMENT_NAME, 132 | PresenceSignature.NAMESPACE); 133 | if (signatureExt instanceof PresenceSignature) { 134 | PresenceSignature signing = (PresenceSignature) signatureExt; 135 | String signature = StringUtils.defaultString(signing.getSignature()); 136 | if (!signature.isEmpty()) { 137 | mHandler.onSignaturePresence(jid, signature); 138 | } else { 139 | LOGGER.warning("no signature in signed presence extension"); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/PrivateKeyReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.io.IOException; 22 | import java.util.List; 23 | import java.util.logging.Level; 24 | import java.util.logging.Logger; 25 | 26 | import org.jivesoftware.smack.SmackException; 27 | import org.jivesoftware.smack.XMPPException; 28 | import org.jivesoftware.smack.packet.IQ; 29 | import org.jivesoftware.smack.util.SuccessCallback; 30 | import org.jivesoftware.smackx.iqregister.packet.Registration; 31 | import org.jivesoftware.smackx.xdata.Form; 32 | import org.jivesoftware.smackx.xdata.FormField; 33 | import org.jivesoftware.smackx.xdata.packet.DataForm; 34 | import org.kontalk.misc.Callback; 35 | 36 | /** 37 | * Send request and listen to response for private data over 38 | * 'jabber:iq:register' namespace. 39 | * 40 | * Temporary server connection is established when requesting key. 41 | * 42 | * TODO not used 43 | * 44 | * @author Alexander Bikadorov {@literal } 45 | */ 46 | public final class PrivateKeyReceiver implements SuccessCallback { 47 | private static final Logger LOGGER = Logger.getLogger(PrivateKeyReceiver.class.getName()); 48 | 49 | private static final String FORM_TYPE_VALUE = "http://kontalk.org/protocol/register#privatekey"; 50 | private static final String FORM_TOKEN_VAR = "token"; 51 | 52 | private final Callback.Handler mHandler; 53 | private KonConnection mConn = null; 54 | 55 | public PrivateKeyReceiver(Callback.Handler handler) { 56 | mHandler = handler; 57 | } 58 | 59 | public void sendRequest(EndpointServer server, boolean validateCertificate, 60 | final String registrationToken) { 61 | // create connection 62 | mConn = new KonConnection(server, validateCertificate); 63 | 64 | Thread thread = new Thread("Private Key Request") { 65 | @Override 66 | public void run() { 67 | PrivateKeyReceiver.this.sendRequestAsync(registrationToken); 68 | } 69 | }; 70 | thread.setDaemon(true); 71 | thread.start(); 72 | } 73 | 74 | private void sendRequestAsync(String registrationToken) { 75 | // connect 76 | try { 77 | mConn.connect(); 78 | } catch (XMPPException | SmackException | IOException | InterruptedException ex) { 79 | LOGGER.log(Level.WARNING, "can't connect to "+mConn.getServer(), ex); 80 | mHandler.handle(new Callback<>(ex)); 81 | return; 82 | } 83 | 84 | Registration iq = new Registration(); 85 | iq.setType(IQ.Type.set); 86 | iq.setTo(mConn.getXMPPServiceDomain()); 87 | Form form = new Form(DataForm.Type.submit); 88 | 89 | // form type field 90 | FormField type = new FormField(FormField.FORM_TYPE); 91 | type.setType(FormField.Type.hidden); 92 | type.addValue(FORM_TYPE_VALUE); 93 | form.addField(type); 94 | 95 | // token field 96 | FormField fieldKey = new FormField(FORM_TOKEN_VAR); 97 | fieldKey.setLabel("Registration token"); 98 | fieldKey.setType(FormField.Type.text_single); 99 | fieldKey.addValue(registrationToken); 100 | form.addField(fieldKey); 101 | 102 | iq.addExtension(form.getDataFormToSend()); 103 | 104 | mConn.sendIqRequestAsync(iq) 105 | .onSuccess(this) 106 | .onError(exception -> mHandler.handle(new Callback<>(exception))); 107 | } 108 | 109 | @Override 110 | public void onSuccess(IQ packet) { 111 | LOGGER.info("response: "+packet); 112 | 113 | mConn.disconnect(); 114 | 115 | if (!(packet instanceof IQ)) { 116 | LOGGER.warning("response not an IQ packet"); 117 | finish(null); 118 | return; 119 | } 120 | IQ iq = (IQ) packet; 121 | 122 | if (iq.getType() != IQ.Type.result) { 123 | LOGGER.warning("ignoring response with IQ type: "+iq.getType()); 124 | this.finish(null); 125 | return; 126 | } 127 | 128 | DataForm response = iq.getExtension(DataForm.ELEMENT, DataForm.NAMESPACE); 129 | if (response == null) { 130 | this.finish(null); 131 | return; 132 | } 133 | 134 | String token = null; 135 | List fields = response.getFields(); 136 | for (FormField field : fields) { 137 | if ("token".equals(field.getVariable())) { 138 | token = field.getValues().get(0).toString(); 139 | break; 140 | } 141 | } 142 | 143 | this.finish(token); 144 | } 145 | 146 | private void finish(String token) { 147 | mHandler.handle(token == null ? 148 | new Callback<>() : 149 | new Callback<>(token)); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/PublicKeyListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.client; 20 | 21 | import java.util.logging.Logger; 22 | import org.jivesoftware.smack.StanzaListener; 23 | import org.apache.commons.lang.StringUtils; 24 | import org.jivesoftware.smack.packet.IQ; 25 | import org.jivesoftware.smack.packet.Stanza; 26 | import org.jivesoftware.smack.provider.ProviderManager; 27 | import org.kontalk.misc.JID; 28 | import org.kontalk.system.Control; 29 | 30 | /** 31 | * 32 | * @author Alexander Bikadorov {@literal } 33 | */ 34 | class PublicKeyListener implements StanzaListener { 35 | private static final Logger LOGGER = Logger.getLogger(PublicKeyListener.class.getName()); 36 | 37 | private final Control mControl; 38 | 39 | static { 40 | ProviderManager.addIQProvider(PublicKeyPublish.ELEMENT_NAME, 41 | PublicKeyPublish.NAMESPACE, 42 | new PublicKeyPublish.Provider()); 43 | } 44 | 45 | public PublicKeyListener(Control control) { 46 | mControl = control; 47 | } 48 | 49 | @Override 50 | public void processStanza(Stanza packet) { 51 | LOGGER.info("got public key: "+StringUtils.abbreviate(packet.toString(), 300)); 52 | 53 | PublicKeyPublish publicKeyPacket = (PublicKeyPublish) packet; 54 | 55 | if (publicKeyPacket.getType() == IQ.Type.set) { 56 | LOGGER.warning("ignoring public key packet with type 'set'"); 57 | return; 58 | } 59 | 60 | if (publicKeyPacket.getType() == IQ.Type.result) { 61 | byte[] keyData = publicKeyPacket.getPublicKey(); 62 | if (keyData == null) { 63 | LOGGER.warning("got public key packet without public key"); 64 | return; 65 | } 66 | mControl.onPGPKey(JID.fromSmack(publicKeyPacket.getFrom()), keyData); 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/client/VCardListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package org.kontalk.client; 8 | 9 | import java.util.logging.Logger; 10 | import org.jivesoftware.smack.StanzaListener; 11 | import org.jivesoftware.smack.packet.IQ; 12 | import org.jivesoftware.smack.packet.Stanza; 13 | import org.jivesoftware.smack.provider.ProviderManager; 14 | import org.kontalk.misc.JID; 15 | import org.kontalk.system.Control; 16 | 17 | /** 18 | * Listener for vCard4 iq stanzas, deprecated! 19 | * 20 | * @author Alexander Bikadorov {@literal } 21 | */ 22 | final class VCardListener implements StanzaListener { 23 | private static final Logger LOGGER = Logger.getLogger(VCardListener.class.getName()); 24 | 25 | private final Control mControl; 26 | 27 | static { 28 | ProviderManager.addIQProvider( 29 | VCard4.ELEMENT_NAME, 30 | VCard4.NAMESPACE, 31 | new VCard4.Provider()); 32 | } 33 | 34 | VCardListener(Control control) { 35 | mControl = control; 36 | } 37 | 38 | @Override 39 | public void processStanza(Stanza packet) { 40 | VCard4 p = (VCard4) packet; 41 | LOGGER.info("vcard: "+p); 42 | 43 | byte[] publicKey = p.getPGPKey(); 44 | 45 | // vcard coming from sync 46 | if (p.getType() == IQ.Type.set) { 47 | LOGGER.warning("ignoring vcard with type 'set'"); 48 | return; 49 | } 50 | 51 | if (p.getType() == IQ.Type.result) { 52 | if (publicKey == null) { 53 | LOGGER.warning("got vcard without pgp key included"); 54 | return; 55 | } 56 | mControl.onPGPKey(JID.fromSmack(p.getFrom()), publicKey); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/crypto/Coder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.crypto; 20 | 21 | import java.io.File; 22 | import java.util.HashMap; 23 | import java.util.Optional; 24 | import java.util.logging.Logger; 25 | 26 | import org.kontalk.crypto.PGPUtils.PGPCoderKey; 27 | import org.kontalk.model.Contact; 28 | import org.kontalk.model.message.DecryptMessage; 29 | import org.kontalk.model.message.MessageContent.InAttachment; 30 | import org.kontalk.model.message.OutMessage; 31 | 32 | /** 33 | * Static methods for decryption and encryption of a message. 34 | * 35 | * @author Alexander Bikadorov {@literal } 36 | */ 37 | public final class Coder { 38 | private static final Logger LOGGER = Logger.getLogger(Coder.class.getName()); 39 | 40 | private Coder() { 41 | } 42 | 43 | /** 44 | * Encryption status of a message. 45 | * Do not modify, only add! Ordinal used in database. 46 | */ 47 | public enum Encryption {NOT, ENCRYPTED, DECRYPTED} 48 | 49 | /** 50 | * Signing status of a message. 51 | * Do not modify, only add! Ordinal used in database. 52 | */ 53 | public enum Signing {NOT, SIGNED, VERIFIED, UNKNOWN} 54 | 55 | /** 56 | * Errors that can occur during de-/encryption and verification. 57 | * Do not modify, only add! Ordinal used in database. 58 | */ 59 | public enum Error { 60 | /** Some unknown error. */ 61 | UNKNOWN_ERROR, 62 | /** Own personal key not found. Unused. */ 63 | MY_KEY_UNAVAILABLE, 64 | /** Public key of sender not found. */ 65 | KEY_UNAVAILABLE, 66 | /** My private key does not match. */ 67 | INVALID_PRIVATE_KEY, 68 | /** Invalid data / parsing failed. */ 69 | INVALID_DATA, 70 | /** No integrity protection found. */ 71 | NO_INTEGRITY, 72 | /** Integrity check failed. */ 73 | INVALID_INTEGRITY, 74 | /** Invalid data / parsing failed of signature. */ 75 | INVALID_SIGNATURE_DATA, 76 | /** Signature does not match sender. */ 77 | INVALID_SIGNATURE, 78 | /** Recipient user id in decrypted data does not match id in my key. */ 79 | INVALID_RECIPIENT, 80 | /** Sender user id in decrypted data does not match id in sender key. */ 81 | INVALID_SENDER, 82 | 83 | //INVALID_MIME, 84 | //INVALID_TIMESTAMP, 85 | } 86 | 87 | private static final HashMap KEY_MAP = new HashMap<>(); 88 | 89 | public static Optional contactkey(Contact contact) { 90 | if (KEY_MAP.containsKey(contact)) { 91 | PGPCoderKey key = KEY_MAP.get(contact); 92 | if (key.fingerprint.equals(contact.getFingerprint())) 93 | return Optional.of(key); 94 | } 95 | 96 | byte[] rawKey = contact.getKey(); 97 | if (rawKey.length != 0) { 98 | PGPCoderKey key = PGPUtils.readPublicKey(rawKey).orElse(null); 99 | if (key != null) { 100 | KEY_MAP.put(contact, key); 101 | return Optional.of(key); 102 | } 103 | } 104 | 105 | LOGGER.warning("key not found for contact: "+contact); 106 | return Optional.empty(); 107 | } 108 | 109 | /** 110 | * Decrypt and verify the body of a message. Sets the encryption and signing 111 | * status of the message and errors that may occur are saved to the message. 112 | */ 113 | public static boolean decryptMessage(PersonalKey myKey, DecryptMessage message) { 114 | return Decryptor.decryptMessage(message, myKey); 115 | } 116 | 117 | /** 118 | * Decrypt and verify a downloaded attachment file. Sets the encryption and 119 | * signing status of the message attachment and errors that may occur are 120 | * saved to the attachment. 121 | */ 122 | public static void decryptAttachment(PersonalKey myKey, InAttachment attachment, Contact sender) { 123 | Decryptor.decryptAttachment(attachment, myKey, sender); 124 | } 125 | 126 | /** 127 | * Creates encrypted and signed message body. 128 | * Errors that may occur are saved to the message. 129 | * @return the encrypted and signed text. 130 | */ 131 | public static String encryptMessageRFC3923(PersonalKey myKey, OutMessage message) { 132 | return new Encryptor(myKey, message).encryptMessageRFC3923(); 133 | } 134 | 135 | public static String encryptStanzaRFC3923(PersonalKey myKey, OutMessage message, String xml) { 136 | return new Encryptor(myKey, message).encryptStanzaRFC3923(xml); 137 | } 138 | 139 | /** Encrypt an arbitrary UTF-8 string as a Base64 encoded OpenPGP Message. */ 140 | public static String encryptString(PersonalKey myKey, OutMessage message, String plainText) { 141 | return new Encryptor(myKey, message).encryptString(plainText); 142 | } 143 | 144 | public static Optional encryptAttachment(PersonalKey myKey, OutMessage message, File file) { 145 | return new Encryptor(myKey, message).encryptAttachment(file); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/misc/Callback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.misc; 20 | 21 | import java.util.Optional; 22 | import java.util.concurrent.CountDownLatch; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | 27 | /** 28 | * 29 | * @author Alexander Bikadorov {@literal } 30 | */ 31 | public final class Callback { 32 | private static final Logger LOGGER = Logger.getLogger(Callback.class.getName()); 33 | 34 | public final V value; 35 | public final Optional exception; 36 | 37 | public Callback() { 38 | this.value = null; 39 | this.exception = Optional.empty(); 40 | } 41 | 42 | public Callback(V response) { 43 | this.value = response; 44 | this.exception = Optional.empty(); 45 | } 46 | 47 | public Callback(Exception ex) { 48 | this.value = null; 49 | this.exception = Optional.of(ex); 50 | } 51 | 52 | public interface Handler { 53 | void handle(Callback callback); 54 | } 55 | 56 | public static class Synchronizer { 57 | private final CountDownLatch mLatch = new CountDownLatch(1); 58 | 59 | public void sync() { 60 | mLatch.countDown(); 61 | } 62 | 63 | public void waitForSync() { 64 | boolean succ = false; 65 | try { 66 | succ = mLatch.await(5, TimeUnit.SECONDS); 67 | } catch (InterruptedException ex) { 68 | LOGGER.log(Level.WARNING, "interrupted", ex); 69 | } 70 | if (!succ) { 71 | LOGGER.warning("await failed, timeout reached"); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/misc/JID.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.misc; 20 | 21 | import java.util.Objects; 22 | import java.util.logging.Level; 23 | import java.util.logging.Logger; 24 | 25 | import org.apache.commons.lang.StringUtils; 26 | import org.jxmpp.jid.BareJid; 27 | import org.jxmpp.jid.Jid; 28 | import org.jxmpp.jid.impl.JidCreate; 29 | import org.jxmpp.jid.parts.Localpart; 30 | import org.jxmpp.jid.util.JidUtil; 31 | import org.jxmpp.stringprep.XmppStringprepException; 32 | import org.jxmpp.stringprep.simple.SimpleXmppStringprep; 33 | import org.jxmpp.util.XmppStringUtils; 34 | 35 | /** 36 | * A Jabber ID (the address of an XMPP entity). Immutable. 37 | * 38 | * Escaping of local part (XEP-0106) supported. 39 | * 40 | * @author Alexander Bikadorov {@literal } 41 | */ 42 | public final class JID { 43 | private static final Logger LOGGER = Logger.getLogger(JID.class.getName()); 44 | 45 | static { 46 | // good to know. For working JID validation 47 | SimpleXmppStringprep.setup(); 48 | } 49 | 50 | private final String mLocal; // escaped! 51 | private final String mDomain; 52 | private final String mResource; 53 | private final boolean mValid; 54 | 55 | private JID(String local, String domain, String resource) { 56 | mLocal = local; 57 | mDomain = domain; 58 | mResource = resource; 59 | 60 | mValid = !mLocal.isEmpty() && !mDomain.isEmpty() 61 | // NOTE: domain check could be stronger - compliant with RFC 6122, but 62 | // server does not accept most special characters 63 | // NOTE: resource not checked 64 | && JidUtil.isTypicalValidEntityBareJid( 65 | XmppStringUtils.completeJidFrom(mLocal, mDomain)); 66 | } 67 | 68 | /** Return unescaped local part. */ 69 | public String local() { 70 | return XmppStringUtils.unescapeLocalpart(mLocal); 71 | } 72 | 73 | public String domain() { 74 | return mDomain; 75 | } 76 | 77 | /** Return JID as escaped string. */ 78 | public String string() { 79 | return XmppStringUtils.completeJidFrom(mLocal, mDomain, mResource); 80 | } 81 | 82 | public String asUnescapedString() { 83 | return XmppStringUtils.completeJidFrom(this.local(), mDomain, mResource); 84 | } 85 | 86 | public boolean isValid() { 87 | return mValid; 88 | } 89 | 90 | public boolean isHash() { 91 | return mLocal.matches("[0-9a-f]{40}"); 92 | } 93 | 94 | public boolean isFull() { 95 | return !mResource.isEmpty(); 96 | } 97 | 98 | public JID toBare() { 99 | return new JID(mLocal, mDomain, ""); 100 | } 101 | 102 | /** To invalid(!) domain JID. */ 103 | public JID toDomain() { 104 | return new JID("", mDomain, ""); 105 | } 106 | 107 | public BareJid toBareSmack() { 108 | try { 109 | return JidCreate.bareFrom(this.string()); 110 | } catch (XmppStringprepException ex) { 111 | LOGGER.log(Level.WARNING, "could not convert to smack", ex); 112 | throw new RuntimeException("You didn't check isValid(), idiot!"); 113 | } 114 | } 115 | 116 | public Jid toSmack() { 117 | try { 118 | return JidCreate.from(this.string()); 119 | } catch (XmppStringprepException ex) { 120 | LOGGER.log(Level.WARNING, "could not convert to smack", ex); 121 | throw new RuntimeException("Not even a simple Jid?"); 122 | } 123 | } 124 | 125 | /** 126 | * Comparing only bare JIDs. 127 | * Case-insensitive. 128 | */ 129 | @Override 130 | public final boolean equals(Object o) { 131 | if (o == this) 132 | return true; 133 | 134 | if (!(o instanceof JID)) 135 | return false; 136 | 137 | JID oJID = (JID) o; 138 | return mLocal.equalsIgnoreCase(oJID.mLocal) && 139 | mDomain.equalsIgnoreCase(oJID.mDomain); 140 | } 141 | 142 | @Override 143 | public int hashCode() { 144 | return Objects.hash(mLocal, mDomain); 145 | } 146 | 147 | /** Use this only for debugging and otherwise string() instead! */ 148 | @Override 149 | public String toString() { 150 | return "'"+this.string()+"'"; 151 | } 152 | 153 | public static JID full(String jid) { 154 | jid = StringUtils.defaultString(jid); 155 | return escape(XmppStringUtils.parseLocalpart(jid), 156 | XmppStringUtils.parseDomain(jid), 157 | XmppStringUtils.parseResource(jid)); 158 | } 159 | 160 | public static JID bare(String jid) { 161 | jid = StringUtils.defaultString(jid); 162 | return escape(XmppStringUtils.parseLocalpart(jid), XmppStringUtils.parseDomain(jid), ""); 163 | } 164 | 165 | public static JID fromSmack(Jid jid) { 166 | Localpart localpart = jid.getLocalpartOrNull(); 167 | return new JID(localpart != null ? localpart.toString() : "", 168 | jid.getDomain().toString(), 169 | jid.getResourceOrEmpty().toString()); 170 | } 171 | 172 | public static JID bare(String local, String domain) { 173 | return escape(local, domain, ""); 174 | } 175 | 176 | private static JID escape(String local, String domain, String resource) { 177 | return new JID(XmppStringUtils.escapeLocalpart(local), domain, resource); 178 | } 179 | 180 | public static JID deleted(int id) { 181 | return new JID("", Integer.toString(id), ""); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/misc/KonException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.misc; 20 | 21 | /** 22 | * Application internal exceptions. 23 | * @author Alexander Bikadorov {@literal } 24 | */ 25 | public final class KonException extends Exception { 26 | 27 | public enum Error { 28 | DB, 29 | IMPORT_ARCHIVE, 30 | IMPORT_READ_FILE, 31 | IMPORT_KEY, 32 | CHANGE_PASS, 33 | CHANGE_PASS_COPY, 34 | WRITE_FILE, 35 | READ_FILE, 36 | LOAD_KEY, 37 | LOAD_KEY_DECRYPT, 38 | CLIENT_CONNECT, 39 | CLIENT_LOGIN, 40 | CLIENT_ERROR, 41 | DOWNLOAD_CREATE, 42 | DOWNLOAD_RESPONSE, 43 | DOWNLOAD_EXECUTE, 44 | DOWNLOAD_WRITE, 45 | UPLOAD_CREATE, 46 | UPLOAD_RESPONSE, 47 | UPLOAD_EXECUTE, 48 | } 49 | 50 | private final Error mError; 51 | 52 | public KonException(Error error, Exception ex) { 53 | super(ex); 54 | mError = error; 55 | } 56 | 57 | public KonException(Error error) { 58 | super(); 59 | mError = error; 60 | } 61 | 62 | public Class getCauseClass() { 63 | Throwable cause = this.getCause(); 64 | return cause == null ? this.getClass() : cause.getClass(); 65 | } 66 | 67 | public Error getError() { 68 | return mError; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/misc/Searchable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package org.kontalk.misc; 19 | 20 | /** 21 | * 22 | * @author Alexander Bikadorov {@literal } 23 | */ 24 | public interface Searchable { 25 | boolean contains(String string); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/misc/ViewEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.misc; 20 | 21 | import java.util.EnumSet; 22 | import org.kontalk.client.FeatureDiscovery; 23 | import org.kontalk.crypto.PGPUtils.PGPCoderKey; 24 | import org.kontalk.model.message.InMessage; 25 | import org.kontalk.model.message.KonMessage; 26 | import org.kontalk.model.Contact; 27 | import org.kontalk.system.Control; 28 | import org.kontalk.system.RosterHandler; 29 | 30 | /** 31 | * Events passed from controller to view. 32 | * @author Alexander Bikadorov {@literal } 33 | */ 34 | public class ViewEvent { 35 | 36 | private ViewEvent() {} 37 | 38 | /** Application status changed. */ 39 | public static class StatusChange extends ViewEvent { 40 | public final Control.Status status; 41 | public final EnumSet features; 42 | 43 | public StatusChange(Control.Status status, EnumSet features) { 44 | this.status = status; 45 | this.features = features; 46 | } 47 | } 48 | 49 | /** Key is password protected (ask for password). */ 50 | public static class PasswordSet extends ViewEvent { 51 | } 52 | 53 | /** The personal account is missing (show import wizard). */ 54 | public static class MissingAccount extends ViewEvent { 55 | public final boolean connect; 56 | 57 | public MissingAccount(boolean connect) { 58 | this.connect = connect; 59 | } 60 | } 61 | 62 | /** An exception was thrown somewhere. */ 63 | public static class Exception extends ViewEvent { 64 | public final KonException exception; 65 | 66 | public Exception(KonException exception) { 67 | this.exception = exception; 68 | } 69 | } 70 | 71 | /** There was a security error while de-/encrypting a message. */ 72 | public static class SecurityError extends ViewEvent { 73 | public final KonMessage message; 74 | 75 | public SecurityError(KonMessage message) { 76 | this.message = message; 77 | } 78 | } 79 | 80 | /** Got a new message. */ 81 | public static class NewMessage extends ViewEvent { 82 | public final InMessage message; 83 | 84 | public NewMessage(InMessage message) { 85 | this.message = message; 86 | } 87 | } 88 | 89 | /** Got a new public key (ask whattodo). */ 90 | public static class NewKey extends ViewEvent { 91 | public final Contact contact; 92 | public final PGPCoderKey key; 93 | 94 | public NewKey(Contact contact, PGPCoderKey key) { 95 | this.contact = contact; 96 | this.key = key; 97 | } 98 | } 99 | 100 | /** Contact was deleted in roster (ask whattodo). */ 101 | public static class ContactDeleted extends ViewEvent { 102 | public final Contact contact; 103 | 104 | public ContactDeleted(Contact contact) { 105 | this.contact = contact; 106 | } 107 | } 108 | 109 | /** A presence error. */ 110 | public static class PresenceError extends ViewEvent { 111 | public final Contact contact; 112 | public final RosterHandler.Error error; 113 | 114 | public PresenceError(Contact contact, RosterHandler.Error error) { 115 | this.contact = contact; 116 | this.error = error; 117 | } 118 | } 119 | 120 | /** A contact wants presence subscription (ask whattodo). */ 121 | public static class SubscriptionRequest extends ViewEvent { 122 | public final Contact contact; 123 | 124 | public SubscriptionRequest(Contact contact) { 125 | this.contact = contact; 126 | } 127 | } 128 | 129 | /** Notify view about connection retry in X seconds. */ 130 | public static class RetryTimerMessage extends ViewEvent { 131 | public final int countdown; 132 | 133 | public RetryTimerMessage(int countdown) { 134 | this.countdown = countdown; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/ContactList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package org.kontalk.model; 19 | 20 | import java.sql.ResultSet; 21 | import java.sql.SQLException; 22 | import java.util.Collections; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | import java.util.Observable; 26 | import java.util.Optional; 27 | import java.util.Set; 28 | import java.util.logging.Level; 29 | import java.util.logging.Logger; 30 | import java.util.stream.Collectors; 31 | 32 | import org.kontalk.misc.JID; 33 | import org.kontalk.persistence.Database; 34 | 35 | /** 36 | * Global list of all contacts. 37 | * 38 | * Does not contain deleted contacts. 39 | * 40 | * @author Alexander Bikadorov {@literal } 41 | */ 42 | public final class ContactList extends Observable { 43 | private static final Logger LOGGER = Logger.getLogger(ContactList.class.getName()); 44 | 45 | private enum ViewChange { MODIFIED } 46 | 47 | private final Map mJIDMap = 48 | Collections.synchronizedMap(new HashMap()); 49 | 50 | ContactList() {} 51 | 52 | Map load() { 53 | assert mJIDMap.isEmpty(); 54 | 55 | Map contactMap = new HashMap<>(); 56 | 57 | Database db = Model.database(); 58 | try (ResultSet resultSet = db.execSelectAll(Contact.TABLE)) { 59 | while (resultSet.next()) { 60 | Contact contact = Contact.load(resultSet); 61 | 62 | JID jid = contact.getJID(); 63 | if (mJIDMap.containsKey(jid)) { 64 | LOGGER.warning("contacts with equal JIDs: " + jid); 65 | continue; 66 | } 67 | if (!contact.isDeleted()) 68 | mJIDMap.put(jid, contact); 69 | 70 | contactMap.put(contact.getID(), contact); 71 | } 72 | } catch (SQLException ex) { 73 | LOGGER.log(Level.WARNING, "can't load contacts from db", ex); 74 | } 75 | this.changed(null); 76 | 77 | return contactMap; 78 | } 79 | 80 | /** Create and add a new contact. */ 81 | public Optional create(JID jid, String name) { 82 | jid = jid.toBare(); 83 | 84 | if (!this.isValid(jid)) 85 | return Optional.empty(); 86 | 87 | Contact newContact = new Contact(jid, name); 88 | if (newContact.getID() < 1) 89 | return Optional.empty(); 90 | 91 | mJIDMap.put(newContact.getJID(), newContact); 92 | 93 | this.changed(ViewChange.MODIFIED); 94 | return Optional.of(newContact); 95 | } 96 | 97 | /** 98 | * Get the contact for a JID (if the JID is in the list). 99 | * Resource is removed for lookup. 100 | */ 101 | public Optional get(JID jid) { 102 | return Optional.ofNullable(mJIDMap.get(jid)); 103 | } 104 | 105 | /** 106 | * Get the contact that represents the user itself. 107 | */ 108 | public Optional getMe() { 109 | JID myJID = Model.getUserJID(); 110 | if (!myJID.isValid()) 111 | return Optional.empty(); 112 | 113 | return this.get(myJID); 114 | } 115 | 116 | public Set getAll(boolean withMe, boolean blocked) { 117 | synchronized(mJIDMap) { 118 | return Collections.unmodifiableSet( 119 | mJIDMap.values().stream() 120 | .filter(c -> 121 | (blocked || !c.isBlocked()) && 122 | (withMe || !c.isMe())) 123 | .collect(Collectors.toSet())); 124 | } 125 | } 126 | 127 | public void delete(Contact contact) { 128 | boolean removed = mJIDMap.remove(contact.getJID(), contact); 129 | if (!removed) { 130 | LOGGER.warning("can't find contact "+contact); 131 | } 132 | 133 | contact.setDeleted(); 134 | 135 | this.changed(ViewChange.MODIFIED); 136 | } 137 | 138 | void onShutDown() { 139 | mJIDMap.values().forEach(Contact::onShutDown); 140 | } 141 | 142 | /** 143 | * Return whether a contact with a specified JID exists. 144 | */ 145 | public boolean contains(JID jid) { 146 | return mJIDMap.containsKey(jid); 147 | } 148 | 149 | public boolean changeJID(Contact contact, JID jid) { 150 | if (!this.isValid(jid)) 151 | return false; 152 | 153 | mJIDMap.put(jid, contact); 154 | mJIDMap.remove(contact.getJID()); 155 | 156 | contact.setJID(jid); 157 | 158 | return true; 159 | } 160 | 161 | private boolean isValid(JID jid) { 162 | if (!jid.isValid()) { 163 | LOGGER.warning("invalid jid: " + jid); 164 | return false; 165 | } 166 | 167 | if (mJIDMap.containsKey(jid)) { 168 | LOGGER.warning("jid already exists: "+jid); 169 | return false; 170 | } 171 | 172 | return true; 173 | } 174 | 175 | private void changed(ViewChange change) { 176 | this.setChanged(); 177 | this.notifyObservers(change); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/Model.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model; 20 | 21 | import java.nio.file.Path; 22 | import java.util.Date; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Optional; 26 | import java.util.logging.Logger; 27 | import org.kontalk.misc.JID; 28 | import org.kontalk.model.chat.Chat; 29 | import org.kontalk.model.chat.ChatList; 30 | import org.kontalk.model.message.InMessage; 31 | import org.kontalk.model.message.MessageContent; 32 | import org.kontalk.model.message.OutMessage; 33 | import org.kontalk.model.message.ProtoMessage; 34 | import org.kontalk.persistence.Config; 35 | import org.kontalk.persistence.Database; 36 | import org.kontalk.util.ClientUtils; 37 | 38 | /** 39 | * 40 | * @author Alexander Bikadorov {@literal } 41 | */ 42 | public final class Model { 43 | private static final Logger LOGGER = Logger.getLogger(Model.class.getName()); 44 | 45 | private static Model INSTANCE = null; 46 | private static Path APP_DIR; 47 | private static Database DATABASE; 48 | 49 | private final ContactList mContactList; 50 | private final ChatList mChatList; 51 | private final Account mAccount; 52 | 53 | private Model(Database db, Path appDir) { 54 | DATABASE = db; 55 | APP_DIR = appDir; 56 | 57 | mAccount = new Account(APP_DIR, Config.getInstance()); 58 | mContactList = new ContactList(); 59 | mChatList = new ChatList(); 60 | 61 | Avatar.createStorageDir(appDir); 62 | } 63 | 64 | public static Model setup(Database db, Path appDir) { 65 | if (INSTANCE != null) { 66 | LOGGER.warning("already set up"); 67 | return INSTANCE; 68 | } 69 | 70 | return INSTANCE = new Model(db, appDir); 71 | } 72 | 73 | public Account account() { 74 | return mAccount; 75 | } 76 | 77 | public ContactList contacts() { 78 | return mContactList; 79 | } 80 | 81 | public ChatList chats() { 82 | return mChatList; 83 | } 84 | 85 | public void load() { 86 | // order matters! 87 | Map contactMap = mContactList.load(); 88 | mChatList.load(contactMap); 89 | } 90 | 91 | public void setUserJID(JID jid) { 92 | Config.getInstance().setProperty(Config.ACC_JID, jid.string()); 93 | 94 | if (!mContactList.contains(jid)) { 95 | LOGGER.info("creating user contact, jid: "+jid); 96 | mContactList.create(jid, ""); 97 | } 98 | } 99 | 100 | public Optional createInMessage(ProtoMessage protoMessage, 101 | Chat chat, ClientUtils.MessageIDs ids, Optional serverDate) { 102 | InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, 103 | ids.xmppID, serverDate); 104 | 105 | if (newMessage.getID() <= 0) 106 | return Optional.empty(); 107 | 108 | if (chat.getMessages().contains(newMessage)) { 109 | LOGGER.info("message already in chat, dropping this one"); 110 | return Optional.empty(); 111 | } 112 | boolean added = chat.addMessage(newMessage); 113 | if (!added) { 114 | LOGGER.warning("can't add message to chat"); 115 | return Optional.empty(); 116 | } 117 | return Optional.of(newMessage); 118 | } 119 | 120 | public Optional createOutMessage(Chat chat, 121 | List contacts, MessageContent content) { 122 | OutMessage newMessage = new OutMessage(chat, contacts, content, 123 | chat.isSendEncrypted()); 124 | 125 | boolean added = chat.addMessage(newMessage); 126 | if (!added) { 127 | LOGGER.warning("could not add outgoing message to chat"); 128 | return Optional.empty(); 129 | } 130 | return Optional.of(newMessage); 131 | } 132 | 133 | public static Path appDir() { 134 | if (APP_DIR == null) 135 | throw new IllegalStateException("model not set up"); 136 | 137 | return APP_DIR; 138 | } 139 | 140 | public static Database database(){ 141 | if (DATABASE == null) 142 | throw new IllegalStateException("model not set up"); 143 | 144 | return DATABASE; 145 | } 146 | 147 | public static JID getUserJID() { 148 | return JID.bare(Config.getInstance().getString(Config.ACC_JID)); 149 | } 150 | 151 | public void onShutDown() { 152 | mContactList.onShutDown(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/chat/ChatMessages.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.chat; 20 | 21 | import java.sql.ResultSet; 22 | import java.sql.SQLException; 23 | import java.util.Collections; 24 | import java.util.Comparator; 25 | import java.util.HashSet; 26 | import java.util.Map; 27 | import java.util.NavigableSet; 28 | import java.util.Optional; 29 | import java.util.Set; 30 | import java.util.SortedSet; 31 | import java.util.TreeSet; 32 | import java.util.logging.Level; 33 | import java.util.logging.Logger; 34 | import java.util.stream.Collectors; 35 | 36 | import org.kontalk.model.Contact; 37 | import org.kontalk.model.message.KonMessage; 38 | import org.kontalk.model.message.OutMessage; 39 | import org.kontalk.persistence.Database; 40 | 41 | /** 42 | * All messages of a chat. 43 | * 44 | * @author Alexander Bikadorov {@literal } 45 | */ 46 | public final class ChatMessages { 47 | private static final Logger LOGGER = Logger.getLogger(ChatMessages.class.getName()); 48 | 49 | private static final Comparator MESSAGE_COMPARATOR = 50 | (KonMessage o1, KonMessage o2) -> { 51 | int dateOrder = o1.getDate().compareTo(o2.getDate()); 52 | return dateOrder != 0 ? dateOrder : Integer.compare(o1.getID(), o2.getID()); 53 | }; 54 | 55 | // comparator inconsistent with .equals(); using one set for ordering... 56 | private final NavigableSet mSortedSet = 57 | Collections.synchronizedNavigableSet(new TreeSet<>(MESSAGE_COMPARATOR)); 58 | // ... and one set for .contains() 59 | private final Set mContainsSet = 60 | Collections.synchronizedSet(new HashSet<>()); 61 | 62 | ChatMessages() { 63 | } 64 | 65 | void load(Database db, Chat chat, Map contactMap) { 66 | try (ResultSet messageRS = db.execSelectWhereInsecure(KonMessage.TABLE, 67 | KonMessage.COL_CHAT_ID + " == " + chat.getID())) { 68 | while (messageRS.next()) { 69 | KonMessage message = KonMessage.load(messageRS, chat, contactMap); 70 | if (message.getTransmissions().isEmpty()) 71 | // ignore broken message 72 | continue; 73 | this.addSilent(message); 74 | } 75 | } catch (SQLException ex) { 76 | LOGGER.log(Level.WARNING, "can't load messages from db", ex); 77 | } 78 | } 79 | 80 | /** 81 | * Add message to chat without notifying other components. 82 | */ 83 | boolean add(KonMessage message) { 84 | return this.addSilent(message); 85 | } 86 | 87 | private boolean addSilent(KonMessage message) { 88 | boolean added = mContainsSet.add(message); 89 | if (!added) { 90 | LOGGER.warning("message already in chat: " + message); 91 | return false; 92 | } 93 | mSortedSet.add(message); 94 | return true; 95 | } 96 | 97 | public Set getAll() { 98 | return Collections.unmodifiableSet(mSortedSet); 99 | } 100 | 101 | /** Get all outgoing messages with status "PENDING" for this chat. */ 102 | public SortedSet getPending() { 103 | synchronized(mSortedSet) { 104 | return mSortedSet.stream() 105 | .filter(m -> m.getStatus() == KonMessage.Status.PENDING 106 | && m instanceof OutMessage) 107 | .map(m -> (OutMessage) m) 108 | .collect(Collectors.toCollection(() -> new TreeSet<>(MESSAGE_COMPARATOR))); 109 | } 110 | } 111 | 112 | /** Get the newest (i.e. last received) outgoing message. */ 113 | public Optional getLast(String xmppID) { 114 | synchronized(mSortedSet) { 115 | return mSortedSet.descendingSet().stream() 116 | .filter(m -> m.getXMPPID().equals(xmppID) && m instanceof OutMessage) 117 | .map(m -> (OutMessage) m).findFirst(); 118 | } 119 | } 120 | 121 | /** Get the last created message. */ 122 | public Optional getLast() { 123 | return mSortedSet.isEmpty() ? 124 | Optional.empty() : 125 | Optional.of(mSortedSet.last()); 126 | } 127 | 128 | public boolean contains(KonMessage message) { 129 | return mContainsSet.contains(message); 130 | } 131 | 132 | public int size() { 133 | return mSortedSet.size(); 134 | } 135 | 136 | public boolean isEmpty() { 137 | return mSortedSet.isEmpty(); 138 | } 139 | 140 | public Optional getPredecessor(KonMessage message) { 141 | SortedSet headSet = mSortedSet.headSet(message); 142 | return headSet.isEmpty() ? Optional.empty() : Optional.of(headSet.last()); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/chat/GroupMetaData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.chat; 20 | 21 | import java.util.Map; 22 | import java.util.Objects; 23 | import java.util.logging.Level; 24 | import java.util.logging.Logger; 25 | import org.json.simple.JSONObject; 26 | import org.json.simple.JSONValue; 27 | import org.kontalk.misc.JID; 28 | 29 | /** 30 | * Immutable meta data fields for a specific group chat protocol implementation. 31 | * 32 | * @author Alexander Bikadorov {@literal } 33 | */ 34 | public abstract class GroupMetaData { 35 | private static final Logger LOGGER = Logger.getLogger(GroupMetaData.class.getName()); 36 | 37 | abstract String toJSON(); 38 | 39 | /** Data fields specific to a Kontalk group chat (custom protocol). */ 40 | public static class KonGroupData extends GroupMetaData { 41 | private static final String JSON_OWNER_JID = "jid"; 42 | private static final String JSON_ID = "id"; 43 | 44 | public final JID owner; 45 | public final String id; 46 | 47 | public KonGroupData(JID ownerJID, String id) { 48 | this.owner = ownerJID; 49 | this.id = id; 50 | } 51 | 52 | // using legacy lib, raw types extend Object 53 | @SuppressWarnings(value = "unchecked") 54 | @Override 55 | public String toJSON() { 56 | JSONObject json = new JSONObject(); 57 | json.put(JSON_OWNER_JID, owner.string()); 58 | json.put(JSON_ID, id); 59 | return json.toJSONString(); 60 | } 61 | 62 | private static GroupMetaData fromJSON(Map map) { 63 | JID jid = JID.bare((String) map.get(JSON_OWNER_JID)); 64 | String id = (String) map.get(JSON_ID); 65 | return new KonGroupData(jid, id); 66 | } 67 | 68 | @Override 69 | public final boolean equals(Object o) { 70 | if (this == o) { 71 | return true; 72 | } 73 | if (!(o instanceof KonGroupData)) { 74 | return false; 75 | } 76 | KonGroupData oGID = (KonGroupData) o; 77 | return owner.equals(oGID.owner) && id.equals(oGID.id); 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | return Objects.hash(owner, id); 83 | } 84 | 85 | @Override 86 | public String toString() { 87 | return "KGD:{id="+id+",owner="+owner+"}"; 88 | } 89 | } 90 | 91 | /** Data fields specific to a MUC (XEP-0045) chat. Not used/deprecated! */ 92 | public static class MUCData extends GroupMetaData { 93 | private static final String JSON_ROOM = "room"; 94 | private static final String JSON_PW = "pw"; 95 | 96 | public final JID room; 97 | public final String password; 98 | 99 | public MUCData(JID room) { 100 | this(room, ""); 101 | } 102 | 103 | public MUCData(JID room, String password) { 104 | this.room = room; 105 | this.password = password; 106 | } 107 | 108 | // using legacy lib, raw types extend Object 109 | @SuppressWarnings(value = "unchecked") 110 | @Override 111 | public String toJSON() { 112 | JSONObject json = new JSONObject(); 113 | json.put(JSON_ROOM, room.string()); 114 | json.put(JSON_PW, password); 115 | return json.toJSONString(); 116 | } 117 | 118 | private static GroupMetaData fromJSON(Map map) { 119 | JID room = JID.bare((String) map.get(JSON_ROOM)); 120 | String pw = (String) map.get(JSON_PW); 121 | return new MUCData(room, pw); 122 | } 123 | 124 | @Override 125 | public final boolean equals(Object o) { 126 | if (this == o) { 127 | return true; 128 | } 129 | if (!(o instanceof MUCData)) { 130 | return false; 131 | } 132 | MUCData oData = (MUCData) o; 133 | return room.equals(oData.room); 134 | } 135 | 136 | @Override 137 | public int hashCode() { 138 | return Objects.hash(room); 139 | } 140 | 141 | @Override 142 | public String toString() { 143 | return "MUCD:{room="+room+"}"; 144 | } 145 | } 146 | 147 | static GroupMetaData fromJSONOrNull(String json) { 148 | Object obj = JSONValue.parse(json); 149 | try { 150 | Map map = (Map) obj; 151 | return map.containsKey(MUCData.JSON_ROOM) ? 152 | MUCData.fromJSON(map) : 153 | KonGroupData.fromJSON(map); 154 | } catch (NullPointerException | ClassCastException ex) { 155 | LOGGER.log(Level.WARNING, "can't parse JSON", ex); 156 | return null; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/chat/Member.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.chat; 20 | 21 | import java.sql.ResultSet; 22 | import java.sql.SQLException; 23 | import java.util.ArrayList; 24 | import java.util.Arrays; 25 | import java.util.Collections; 26 | import java.util.Date; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.logging.Level; 30 | import java.util.logging.Logger; 31 | import org.jivesoftware.smackx.chatstates.ChatState; 32 | import org.kontalk.model.Contact; 33 | import org.kontalk.model.Model; 34 | import org.kontalk.persistence.Database; 35 | 36 | /** 37 | * An association between a contact and a chat. 38 | * Single chats have exactly one, group chats can have any number of members. 39 | * 40 | * @author Alexander Bikadorov {@literal } 41 | */ 42 | public final class Member extends ProtoMember { 43 | private static final Logger LOGGER = Logger.getLogger(Member.class.getName()); 44 | 45 | private final int mID; 46 | private final int mChatID; 47 | 48 | public static final String TABLE = "receiver"; 49 | public static final String COL_CONTACT_ID = "user_id"; 50 | public static final String COL_ROLE = "role"; 51 | public static final String COL_CHAT_ID = "thread_id"; 52 | public static final String SCHEMA = "(" + 53 | Database.SQL_ID + 54 | COL_CHAT_ID + " INTEGER NOT NULL, " + 55 | COL_CONTACT_ID + " INTEGER NOT NULL, " + 56 | COL_ROLE + " INTEGER NOT NULL, " + 57 | "UNIQUE (" + COL_CHAT_ID + ", " + COL_CONTACT_ID + "), " + 58 | "FOREIGN KEY (" + COL_CHAT_ID + ") REFERENCES " + Chat.TABLE + " (_id), " + 59 | "FOREIGN KEY (" + COL_CONTACT_ID + ") REFERENCES " + Contact.TABLE + " (_id) " + 60 | ")"; 61 | 62 | private ChatState mState = ChatState.gone; 63 | // note: the Android client does not set active states when only viewing 64 | // the chat (not necessary according to XEP-0085), this makes the 65 | // extra date field a bit useless 66 | // TODO save last active date to DB 67 | private Date mLastActive = null; 68 | 69 | Member(Contact contact, int chatID) { 70 | this(contact, Role.DEFAULT, chatID); 71 | } 72 | 73 | Member(ProtoMember protoMember, int chatID) { 74 | this(protoMember.mContact, protoMember.mRole, chatID); 75 | } 76 | 77 | private Member(Contact contact, Role role, int chatID) { 78 | super(contact, role); 79 | 80 | mChatID = chatID; 81 | 82 | List recValues = Arrays.asList( 83 | mChatID, 84 | mContact.getID(), 85 | mRole); 86 | mID = Model.database().execInsert(TABLE, recValues); 87 | if (mID <= 0) { 88 | LOGGER.warning("could not insert member"); 89 | } 90 | } 91 | 92 | private Member(int id, Contact contact, Role role, int chatID) { 93 | super(contact, role); 94 | mID = id; 95 | mChatID = chatID; 96 | } 97 | 98 | public ChatState getState() { 99 | return mState; 100 | } 101 | 102 | void save(Database db) { 103 | // TODO 104 | } 105 | 106 | boolean delete(Database db) { 107 | return db.execDelete(TABLE, mID); 108 | } 109 | 110 | void setState(ChatState state) { 111 | mState = state; 112 | if (mState == ChatState.active || mState == ChatState.composing) 113 | mLastActive = new Date(); 114 | } 115 | 116 | /** Load Members of a chat. */ 117 | static List load(Database db, int chatID, Map contactMap) { 118 | String where = COL_CHAT_ID + " == " + chatID; 119 | ResultSet resultSet; 120 | try { 121 | resultSet = db.execSelectWhereInsecure(TABLE, where); 122 | } catch (SQLException ex) { 123 | LOGGER.log(Level.WARNING, "can't get receiver from db", ex); 124 | return Collections.emptyList(); 125 | } 126 | List members = new ArrayList<>(); 127 | try { 128 | while (resultSet.next()) { 129 | int id = resultSet.getInt("_id"); 130 | int contactID = resultSet.getInt(COL_CONTACT_ID); 131 | int r = resultSet.getInt(COL_ROLE); 132 | Role role = Role.values()[r]; 133 | Contact c = contactMap.get(contactID); 134 | if (c == null) { 135 | LOGGER.warning("can't find contact, ID:"+contactID); 136 | continue; 137 | } 138 | 139 | members.add(new Member(id, c, role, chatID)); 140 | } 141 | resultSet.close(); 142 | } catch (SQLException ex) { 143 | LOGGER.log(Level.WARNING, "can't get members", ex); 144 | } 145 | return members; 146 | } 147 | 148 | @Override 149 | public String toString() { 150 | return "Mem:id="+mID+",chatID="+mChatID 151 | +"cont={"+mContact+"},role="+mRole; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/chat/ProtoMember.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.chat; 20 | 21 | import java.util.Objects; 22 | import org.kontalk.model.Contact; 23 | 24 | /** 25 | * A contact to be inserted into/removed from a (group) chat. 26 | * 27 | * @author Alexander Bikadorov {@literal } 28 | */ 29 | public class ProtoMember { 30 | 31 | /** 32 | * Long-live authorization model of member in group. 33 | * Called 'Affiliation' in MUC 34 | * Do not modify, only add! Ordinal used in database 35 | */ 36 | public enum Role {DEFAULT, OWNER, ADMIN} 37 | 38 | final Contact mContact; 39 | final Role mRole; 40 | 41 | public ProtoMember(Contact contact){ 42 | this(contact, Role.DEFAULT); 43 | } 44 | 45 | public ProtoMember(Contact contact, Role role) { 46 | mContact = contact; 47 | mRole = role; 48 | } 49 | 50 | public Contact getContact() { 51 | return mContact; 52 | } 53 | 54 | public Role getRole() { 55 | return mRole; 56 | } 57 | 58 | @Override 59 | public final boolean equals(Object o) { 60 | if (o == this) 61 | return true; 62 | 63 | if (!(o instanceof ProtoMember)) 64 | return false; 65 | 66 | return mContact.equals(((ProtoMember) o).mContact); 67 | } 68 | 69 | @Override 70 | public int hashCode() { 71 | return Objects.hash(mContact); 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "PMem:cont={"+mContact+"},role="+mRole; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/chat/SingleChat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.chat; 20 | 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.Objects; 24 | import java.util.logging.Logger; 25 | 26 | import org.jivesoftware.smackx.chatstates.ChatState; 27 | import org.kontalk.model.Contact; 28 | 29 | /** 30 | * A long-term persistent chat conversation between user and exactly one contact. 31 | * 32 | * @author Alexander Bikadorov {@literal } 33 | */ 34 | public final class SingleChat extends Chat { 35 | private static final Logger LOGGER = Logger.getLogger(SingleChat.class.getName()); 36 | 37 | private final Member mMember; 38 | private final String mXMPPID; 39 | 40 | SingleChat(Contact contact, String xmppID) { 41 | super(xmppID, "", null); 42 | 43 | mMember = new Member(contact, mID); 44 | // NOTE: Kontalk Android client is ignoring the chat XMPP-ID 45 | mXMPPID = xmppID; 46 | 47 | mMember.getContact().addObserver(this); 48 | } 49 | 50 | // used when loading from database 51 | SingleChat( 52 | int id, 53 | Member member, 54 | String xmppID, 55 | boolean read, 56 | String jsonViewSettings) { 57 | super(id, read, jsonViewSettings); 58 | 59 | mMember = member; 60 | mXMPPID = xmppID; 61 | mMember.getContact().addObserver(this); 62 | } 63 | 64 | public Member getMember() { 65 | return mMember; 66 | } 67 | 68 | @Override 69 | public List getAllMembers() { 70 | return Collections.singletonList(mMember); 71 | } 72 | 73 | @Override 74 | public List getAllContacts() { 75 | return Collections.singletonList(mMember.getContact()); 76 | } 77 | 78 | @Override 79 | public List getValidContacts() { 80 | Contact c = mMember.getContact(); 81 | if ((c.isDeleted() || c.isBlocked()) && !c.isMe()) 82 | return Collections.emptyList(); 83 | 84 | return Collections.singletonList(c); 85 | } 86 | 87 | @Override 88 | public String getXMPPID() { 89 | return mXMPPID; 90 | } 91 | 92 | @Override 93 | public String getSubject() { 94 | return ""; 95 | } 96 | 97 | @Override 98 | public boolean isSendEncrypted() { 99 | return mMember.getContact().getEncrypted(); 100 | } 101 | 102 | @Override 103 | public boolean canSendEncrypted() { 104 | Contact c = mMember.getContact(); 105 | return !c.isDeleted() && !c.isBlocked() && c.hasKey(); 106 | } 107 | 108 | @Override 109 | public boolean isValid() { 110 | Contact c = mMember.getContact(); 111 | return !c.isDeleted() && !c.isBlocked(); 112 | } 113 | 114 | @Override 115 | public boolean isAdministratable() { 116 | return false; 117 | } 118 | 119 | @Override 120 | public void setChatState(Contact contact, ChatState chatState) { 121 | if (!contact.equals(mMember.getContact())) { 122 | LOGGER.warning("wrong contact!?"); 123 | return; 124 | } 125 | mMember.setState(chatState); 126 | this.changed(ViewChange.MEMBER_STATE); 127 | } 128 | 129 | @Override 130 | void save() { 131 | super.save(""); 132 | } 133 | 134 | @Override 135 | public final boolean equals(Object o) { 136 | if (this == o) return true; 137 | 138 | if (!(o instanceof SingleChat)) return false; 139 | 140 | SingleChat oChat = (SingleChat) o; 141 | return mMember.equals(oChat.mMember) && 142 | mXMPPID.equals(oChat.mXMPPID); 143 | } 144 | 145 | @Override 146 | public int hashCode() { 147 | return Objects.hash(mMember, mXMPPID); 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | return "SC:id="+mID+",xmppid="+mXMPPID+",mem="+mMember; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/message/CoderStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.message; 20 | 21 | import java.util.EnumSet; 22 | import org.kontalk.crypto.Coder; 23 | 24 | /** 25 | * The encoding/decoding status of a an item (text, attachment, ...) in a 26 | * message. 27 | * @author Alexander Bikadorov {@literal } 28 | */ 29 | public class CoderStatus { 30 | 31 | private Coder.Encryption mEncryption; 32 | private Coder.Signing mSigning; 33 | private final EnumSet mErrors; 34 | 35 | public CoderStatus(Coder.Encryption encryption, 36 | Coder.Signing signing, 37 | EnumSet errors) { 38 | this.mEncryption = encryption; 39 | this.mSigning = signing; 40 | this.mErrors = errors; 41 | } 42 | 43 | public Coder.Encryption getEncryption() { 44 | return mEncryption; 45 | } 46 | 47 | public boolean isEncrypted() { 48 | return mEncryption == Coder.Encryption.ENCRYPTED; 49 | } 50 | 51 | /** 52 | * Return whether the data is (or was) encrypted. 53 | * @return true if message is (or was) encrypted, else false 54 | */ 55 | public boolean isSecure() { 56 | return mEncryption == Coder.Encryption.ENCRYPTED || 57 | mEncryption == Coder.Encryption.DECRYPTED; 58 | } 59 | 60 | void setDecrypted() { 61 | assert mEncryption == Coder.Encryption.ENCRYPTED; 62 | mEncryption = Coder.Encryption.DECRYPTED; 63 | } 64 | 65 | public Coder.Signing getSigning() { 66 | return mSigning; 67 | } 68 | 69 | public void setSigning(Coder.Signing signing) { 70 | if (signing == mSigning) 71 | return; 72 | 73 | // check for locical errors in coder 74 | if (signing == Coder.Signing.NOT) 75 | assert mSigning == Coder.Signing.UNKNOWN; 76 | if (signing == Coder.Signing.SIGNED) 77 | assert mSigning == Coder.Signing.UNKNOWN; 78 | if (signing == Coder.Signing.VERIFIED) 79 | assert mSigning == Coder.Signing.SIGNED || 80 | mSigning == Coder.Signing.UNKNOWN; 81 | 82 | mSigning = signing; 83 | } 84 | 85 | public EnumSet getErrors() { 86 | // better return a copy 87 | return mErrors.clone(); 88 | } 89 | 90 | public void setSecurityErrors(EnumSet errors) { 91 | mErrors.clear(); 92 | mErrors.addAll(errors); 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return "CSTAT:encr="+mEncryption+",sign="+mSigning+",err="+mErrors; 98 | } 99 | 100 | static CoderStatus createInsecure() { 101 | return new CoderStatus(Coder.Encryption.NOT, Coder.Signing.NOT, 102 | EnumSet.noneOf(Coder.Error.class)); 103 | } 104 | 105 | static CoderStatus createEncrypted() { 106 | return new CoderStatus(Coder.Encryption.ENCRYPTED, Coder.Signing.UNKNOWN, 107 | EnumSet.noneOf(Coder.Error.class)); 108 | } 109 | 110 | static CoderStatus createToEncrypt() { 111 | return new CoderStatus( 112 | // outgoing messages are never saved encrypted 113 | Coder.Encryption.DECRYPTED, 114 | // ignored: when encrypting we always sign too 115 | Coder.Signing.SIGNED, 116 | EnumSet.noneOf(Coder.Error.class)); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/message/DecryptMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.message; 20 | 21 | import java.util.EnumSet; 22 | import org.kontalk.crypto.Coder; 23 | import org.kontalk.crypto.Coder.Signing; 24 | import org.kontalk.model.Contact; 25 | 26 | /** 27 | * Interface for decryptable messages. 28 | * 29 | * @author Alexander Bikadorov {@literal } 30 | */ 31 | public interface DecryptMessage { 32 | 33 | Contact getContact(); 34 | 35 | boolean isEncrypted(); 36 | 37 | String getEncryptedContent(); 38 | 39 | void setDecryptedContent(MessageContent content); 40 | 41 | void setSigning(Signing signing); 42 | 43 | void setSecurityErrors(EnumSet errors); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/message/InMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.message; 20 | 21 | import java.util.Collections; 22 | import java.util.Date; 23 | import java.util.HashSet; 24 | import java.util.Objects; 25 | import java.util.Optional; 26 | import java.util.Set; 27 | 28 | import org.kontalk.crypto.Coder; 29 | import org.kontalk.misc.JID; 30 | import org.kontalk.model.Contact; 31 | import org.kontalk.model.chat.Chat; 32 | 33 | /** 34 | * Model for an XMPP message sent to the user. 35 | * @author Alexander Bikadorov {@literal } 36 | */ 37 | public final class InMessage extends KonMessage implements DecryptMessage { 38 | 39 | private final Transmission mTransmission; 40 | 41 | public InMessage(ProtoMessage proto, Chat chat, JID from, 42 | String xmppID, Optional serverDate) { 43 | super( 44 | chat, 45 | xmppID, 46 | proto.getContent(), 47 | serverDate, 48 | Status.IN, 49 | proto.getCoderStatus()); 50 | 51 | mTransmission = new Transmission(proto.getContact(), from, mID); 52 | } 53 | 54 | // used when loading from database 55 | InMessage(KonMessage.Builder builder) { 56 | super(builder); 57 | 58 | if (builder.mTransmissions.size() != 1) 59 | throw new IllegalArgumentException("builder does not contain one transmission"); 60 | 61 | mTransmission = builder.mTransmissions.iterator().next(); 62 | } 63 | 64 | @Override 65 | public Contact getContact() { 66 | return mTransmission.getContact(); 67 | } 68 | 69 | public JID getJID() { 70 | return mTransmission.getJID(); 71 | } 72 | 73 | @Override 74 | public void setSigning(Coder.Signing signing) { 75 | mCoderStatus.setSigning(signing); 76 | this.save(); 77 | } 78 | 79 | @Override 80 | public String getEncryptedContent() { 81 | return mContent.getEncryptedContent(); 82 | } 83 | 84 | @Override 85 | public void setDecryptedContent(MessageContent decryptedContent) { 86 | mContent.setDecryptedContent(decryptedContent); 87 | mCoderStatus.setDecrypted(); 88 | this.save(); 89 | this.changed(ViewChange.CONTENT); 90 | } 91 | 92 | @Override 93 | public Set getTransmissions() { 94 | return new HashSet<>(Collections.singletonList(mTransmission)); 95 | } 96 | 97 | @Override 98 | public final boolean equals(Object o) { 99 | if (o == this) 100 | return true; 101 | 102 | if (!(o instanceof InMessage)) 103 | return false; 104 | 105 | InMessage oMessage = (InMessage) o; 106 | return this.abstractEquals(oMessage) && 107 | mTransmission.equals(oMessage.mTransmission); 108 | } 109 | 110 | @Override 111 | public int hashCode() { 112 | int hash = this.abstractHashCode(); 113 | hash = 67 * hash + Objects.hash(mTransmission); 114 | return hash; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/message/OutMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.message; 20 | 21 | import java.util.Collections; 22 | import java.util.Date; 23 | import java.util.HashSet; 24 | import java.util.List; 25 | import java.util.Objects; 26 | import java.util.Optional; 27 | import java.util.Set; 28 | import java.util.logging.Logger; 29 | 30 | import org.kontalk.crypto.Coder; 31 | import org.kontalk.misc.JID; 32 | import org.kontalk.model.Contact; 33 | import org.kontalk.model.chat.Chat; 34 | import org.kontalk.util.EncodingUtils; 35 | 36 | /** 37 | * Model for an XMPP message from the user to a contact. 38 | * @author Alexander Bikadorov {@literal } 39 | */ 40 | public final class OutMessage extends KonMessage { 41 | private static final Logger LOGGER = Logger.getLogger(OutMessage.class.getName()); 42 | 43 | private final Set mTransmissions; 44 | 45 | public OutMessage(Chat chat, List contacts, 46 | MessageContent content, boolean encrypted) { 47 | super( 48 | chat, 49 | "Kon_" + EncodingUtils.randomString(8), 50 | content, 51 | Optional.empty(), 52 | Status.PENDING, 53 | encrypted ? 54 | CoderStatus.createToEncrypt() : 55 | CoderStatus.createInsecure()); 56 | 57 | Set ts = new HashSet<>(); 58 | contacts.forEach(contact -> { 59 | boolean succ = ts.add(new Transmission(contact, contact.getJID(), mID)); 60 | if (!succ) 61 | LOGGER.warning("duplicate contact: " + contact); 62 | }); 63 | mTransmissions = Collections.unmodifiableSet(ts); 64 | } 65 | 66 | // used when loading from database 67 | OutMessage(KonMessage.Builder builder) { 68 | super(builder); 69 | 70 | mTransmissions = Collections.unmodifiableSet(builder.mTransmissions); 71 | } 72 | 73 | public void setReceived(JID jid, Date date) { 74 | Transmission transmission = mTransmissions.stream() 75 | .filter(t -> t.getContact().getJID().equals(jid)) 76 | .findFirst().orElse(null); 77 | if (transmission == null) { 78 | LOGGER.warning("can't find transmission for received status, IDs: "+jid); 79 | return; 80 | } 81 | 82 | if (transmission.isReceived()) 83 | // probably already received by another client 84 | return; 85 | 86 | transmission.setReceived(date); 87 | this.changed(ViewChange.STATUS); 88 | } 89 | 90 | public void setStatus(Status status) { 91 | if (status == Status.IN || status == Status.RECEIVED) { 92 | LOGGER.warning("wrong status argument: "+status); 93 | return; 94 | } 95 | 96 | if (status == Status.SENT && mStatus != Status.PENDING) 97 | LOGGER.warning("unexpected new status of sent message: "+status); 98 | 99 | mStatus = status; 100 | if (status != Status.PENDING) 101 | mServerDate = new Date(); 102 | this.save(); 103 | this.changed(ViewChange.STATUS); 104 | } 105 | 106 | // Note: only one error per message (not transmission) possible 107 | public void setServerError(String condition, String text) { 108 | if (mStatus != Status.SENT) 109 | LOGGER.warning("unexpected status of message with error: "+mStatus); 110 | mServerError = new KonMessage.ServerError(condition, text); 111 | this.setStatus(Status.ERROR); 112 | } 113 | 114 | public boolean isSendEncrypted() { 115 | return mCoderStatus.getEncryption() != Coder.Encryption.NOT || 116 | mCoderStatus.getSigning() != Coder.Signing.NOT; 117 | } 118 | 119 | @Override 120 | public Set getTransmissions() { 121 | return mTransmissions; 122 | } 123 | 124 | @Override 125 | public final boolean equals(Object o) { 126 | if (o == this) 127 | return true; 128 | 129 | if (!(o instanceof OutMessage)) 130 | return false; 131 | 132 | return this.abstractEquals((KonMessage) o); 133 | } 134 | 135 | @Override 136 | public int hashCode() { 137 | int hash = this.abstractHashCode(); 138 | hash = 67 * hash + Objects.hash(mTransmissions); 139 | return hash; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/model/message/ProtoMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.model.message; 20 | 21 | import java.util.EnumSet; 22 | import org.kontalk.crypto.Coder; 23 | import org.kontalk.model.Contact; 24 | 25 | /** 26 | * An incoming message used for decryption. Not saved to database. 27 | * 28 | * @author Alexander Bikadorov {@literal } 29 | */ 30 | public final class ProtoMessage implements DecryptMessage { 31 | 32 | private final Contact mContact; 33 | private final CoderStatus mCoderStatus; 34 | private final MessageContent mContent; 35 | 36 | public ProtoMessage(Contact contact, MessageContent content) { 37 | mContact = contact; 38 | mContent = content; 39 | 40 | boolean encrypted = !content.getEncryptedContent().isEmpty(); 41 | mCoderStatus = new CoderStatus( 42 | // no decryption attempt yet 43 | encrypted ? Coder.Encryption.ENCRYPTED : Coder.Encryption.NOT, 44 | // if encrypted we don't know yet 45 | encrypted ? Coder.Signing.UNKNOWN : Coder.Signing.NOT, 46 | // no errors 47 | EnumSet.noneOf(Coder.Error.class) 48 | ); 49 | } 50 | 51 | @Override 52 | public Contact getContact() { 53 | return mContact; 54 | } 55 | 56 | public CoderStatus getCoderStatus() { 57 | return mCoderStatus; 58 | } 59 | 60 | @Override 61 | public boolean isEncrypted() { 62 | return mCoderStatus.isEncrypted(); 63 | } 64 | 65 | MessageContent getContent() { 66 | return mContent; 67 | } 68 | 69 | @Override 70 | public String getEncryptedContent() { 71 | return mContent.getEncryptedContent(); 72 | } 73 | 74 | @Override 75 | public void setDecryptedContent(MessageContent content) { 76 | mContent.setDecryptedContent(content); 77 | mCoderStatus.setDecrypted(); 78 | } 79 | 80 | @Override 81 | public void setSigning(Coder.Signing signing) { 82 | mCoderStatus.setSigning(signing); 83 | } 84 | 85 | @Override 86 | public void setSecurityErrors(EnumSet errors) { 87 | mCoderStatus.setSecurityErrors(errors); 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return "PM:contact="+mContact+",content="+mContent+",codstat="+mCoderStatus; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/persistence/Config.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.persistence; 20 | 21 | import java.nio.file.Path; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | import org.apache.commons.configuration.ConfigurationException; 27 | import org.apache.commons.configuration.PropertiesConfiguration; 28 | import org.kontalk.util.Tr; 29 | 30 | /** 31 | * Global configuration options. 32 | * 33 | * @author Alexander Bikadorov {@literal } 34 | */ 35 | public final class Config extends PropertiesConfiguration { 36 | private static final Logger LOGGER = Logger.getLogger(Config.class.getName()); 37 | 38 | private static Config INSTANCE = null; 39 | 40 | private static final String FILENAME = "kontalk.properties"; 41 | 42 | // all configuration property keys 43 | // disable network property for now -> same as server host 44 | //public static final String SERV_NET = "server.network"; 45 | public static final String SERV_HOST = "server.host"; 46 | public static final String SERV_PORT = "server.port"; 47 | public static final String SERV_CERT_VALIDATION = "server.cert_validation"; 48 | public static final String ACC_PASS = "account.passphrase"; 49 | public static final String ACC_JID = "account.jid"; 50 | public static final String VIEW_FRAME_WIDTH = "view.frame.width"; 51 | public static final String VIEW_FRAME_HEIGHT = "view.frame.height"; 52 | public static final String VIEW_SELECTED_CHAT = "view.thread"; 53 | public static final String VIEW_CHAT_BG = "view.thread_bg"; 54 | public static final String VIEW_USER_CONTACT = "view.user_in_contactlist"; 55 | public static final String VIEW_HIDE_BLOCKED = "view.hide_blocked_contacts"; 56 | public static final String VIEW_MESSAGE_FONT_SIZE = "view.msg_font_size"; 57 | public static final String NET_SEND_CHAT_STATE = "net.chatstate"; 58 | public static final String NET_SEND_ROSTER_NAME = "net.roster_name"; 59 | public static final String NET_STATUS_LIST = "net.status_list"; 60 | public static final String NET_AUTO_SUBSCRIPTION = "net.auto_subscription"; 61 | public static final String NET_REQUEST_AVATARS = "net.request_avatars"; 62 | public static final String NET_MAX_IMG_SIZE = "net.max_img_size"; 63 | public static final String MAIN_CONNECT_STARTUP = "main.connect_startup"; 64 | public static final String NET_RETRY_CONNECT = "main.retry_connect"; 65 | public static final String MAIN_TRAY = "main.tray"; 66 | public static final String MAIN_TRAY_CLOSE = "main.tray_close"; 67 | public static final String MAIN_ENTER_SENDS = "main.enter_sends"; 68 | 69 | // default server address 70 | //public static final String DEFAULT_SERV_NET = "kontalk.net"; 71 | public static final String DEFAULT_SERV_HOST = "beta.kontalk.net"; 72 | public static final int DEFAULT_SERV_PORT = 5999; 73 | 74 | private final String mDefaultXMPPStatus = 75 | Tr.tr("Hey, I'm using Kontalk on my PC!"); 76 | 77 | private Config(Path configFile) { 78 | super(); 79 | 80 | // separate list elements by tab character 81 | this.setListDelimiter((char) 9); 82 | 83 | try { 84 | this.load(configFile.toString()); 85 | } catch (ConfigurationException ex) { 86 | LOGGER.info("configuration file not found; using default values"); 87 | } 88 | 89 | this.setFileName(configFile.toString()); 90 | 91 | // init config / set default values for new properties 92 | Map map = new HashMap<>(); 93 | //map.put(SERV_NET, DEFAULT_SERV_NET); 94 | map.put(SERV_HOST, DEFAULT_SERV_HOST); 95 | map.put(SERV_PORT, DEFAULT_SERV_PORT); 96 | map.put(SERV_CERT_VALIDATION, true); 97 | map.put(ACC_PASS, ""); 98 | map.put(ACC_JID, ""); 99 | map.put(VIEW_FRAME_WIDTH, 600); 100 | map.put(VIEW_FRAME_HEIGHT, 650); 101 | map.put(VIEW_SELECTED_CHAT, -1); 102 | map.put(VIEW_CHAT_BG, ""); 103 | map.put(VIEW_USER_CONTACT, false); 104 | map.put(VIEW_HIDE_BLOCKED, false); 105 | map.put(VIEW_MESSAGE_FONT_SIZE, -1); 106 | map.put(NET_SEND_CHAT_STATE, true); 107 | map.put(NET_SEND_ROSTER_NAME, false); 108 | map.put(NET_STATUS_LIST, new String[]{mDefaultXMPPStatus}); 109 | map.put(NET_AUTO_SUBSCRIPTION, false); 110 | map.put(NET_REQUEST_AVATARS, true); 111 | map.put(NET_MAX_IMG_SIZE, -1); 112 | map.put(NET_RETRY_CONNECT, true); 113 | map.put(MAIN_CONNECT_STARTUP, true); 114 | map.put(MAIN_TRAY, true); 115 | map.put(MAIN_TRAY_CLOSE, false); 116 | map.put(MAIN_ENTER_SENDS, true); 117 | 118 | map.entrySet().stream() 119 | .filter(e -> !this.containsKey(e.getKey())) 120 | .forEach(e -> this.setProperty(e.getKey(), e.getValue())); 121 | } 122 | 123 | public void saveToFile() { 124 | try { 125 | this.save(); 126 | } catch (ConfigurationException ex) { 127 | LOGGER.log(Level.WARNING, "can't save configuration", ex); 128 | } 129 | } 130 | 131 | public static void initialize(Path appDir) { 132 | if (INSTANCE != null) { 133 | LOGGER.warning("already initialized"); 134 | return; 135 | } 136 | 137 | INSTANCE = new Config(appDir.resolve(Config.FILENAME)); 138 | } 139 | 140 | public static Config getInstance() { 141 | if (INSTANCE == null) 142 | throw new IllegalStateException("not initialized"); 143 | 144 | return INSTANCE; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/system/AccountImporter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.system; 20 | 21 | import java.io.IOException; 22 | import java.util.Observable; 23 | import java.util.logging.Level; 24 | import java.util.logging.Logger; 25 | import java.util.zip.ZipEntry; 26 | import java.util.zip.ZipFile; 27 | 28 | import org.kontalk.client.EndpointServer; 29 | import org.kontalk.client.PrivateKeyReceiver; 30 | import org.kontalk.crypto.PGPUtils; 31 | import org.kontalk.misc.Callback; 32 | import org.kontalk.misc.KonException; 33 | import org.kontalk.model.Account; 34 | import org.kontalk.util.EncodingUtils; 35 | 36 | /** 37 | * Import and set user account from various sources. 38 | * @author Alexander Bikadorov {@literal } 39 | */ 40 | public final class AccountImporter extends Observable implements Callback.Handler{ 41 | private static final Logger LOGGER = Logger.getLogger(AccountImporter.class.getName()); 42 | 43 | private static final String PRIVATE_KEY_FILENAME = "kontalk-private.asc"; 44 | 45 | private final Account mAccount; 46 | 47 | private char[] mPassword = null; 48 | private boolean mAborted = false; 49 | 50 | AccountImporter(Account account) { 51 | mAccount = account; 52 | } 53 | 54 | public void fromZipFile(String zipFilePath, char[] password) { 55 | // read key files 56 | byte[] privateKeyData; 57 | try (ZipFile zipFile = new ZipFile(zipFilePath)) { 58 | privateKeyData = readBytesFromZip(zipFile, PRIVATE_KEY_FILENAME); 59 | } catch (IOException ex) { 60 | LOGGER.log(Level.WARNING, "can't open zip archive: ", ex); 61 | this.changed(new KonException(KonException.Error.IMPORT_ARCHIVE, ex)); 62 | return; 63 | } catch (KonException ex) { 64 | this.changed(ex); 65 | return; 66 | } 67 | 68 | this.set(privateKeyData, password); 69 | } 70 | 71 | // note: with disarming if needed 72 | private static byte[] readBytesFromZip(ZipFile zipFile, String filename) throws KonException { 73 | ZipEntry zipEntry = zipFile.getEntry(filename); 74 | byte[] bytes; 75 | try { 76 | bytes = PGPUtils.mayDisarm(zipFile.getInputStream(zipEntry)); 77 | } catch (IOException ex) { 78 | LOGGER.log(Level.WARNING, "can't read key file from archive: ", ex); 79 | throw new KonException(KonException.Error.IMPORT_READ_FILE, ex); 80 | } 81 | return bytes; 82 | } 83 | 84 | // TODO unused 85 | public void fromServer(String host, int port, boolean validateCertificate, 86 | String token, char[] password) { 87 | mPassword = password; 88 | // send private key request 89 | EndpointServer server = new EndpointServer(host, port); 90 | PrivateKeyReceiver receiver = new PrivateKeyReceiver(this); 91 | mAborted = false; 92 | receiver.sendRequest(server, validateCertificate, token); 93 | 94 | // wait for response... continue with handle callback 95 | } 96 | 97 | public void abort() { 98 | // receiver will always terminate after some time, just ignore response 99 | mAborted = true; 100 | } 101 | 102 | @Override 103 | public void handle(Callback callback) { 104 | if (mAborted) 105 | return; 106 | 107 | if (callback.exception.isPresent()) { 108 | this.changed(callback.exception); 109 | return; 110 | } 111 | 112 | this.set(EncodingUtils.base64ToBytes(callback.value), mPassword); 113 | } 114 | 115 | private void set(byte[] privateKeyData, char[] password) { 116 | try { 117 | mAccount.setAccount(privateKeyData, password); 118 | } catch (KonException ex) { 119 | this.changed(ex); 120 | return; 121 | } 122 | // report success 123 | this.changed(null); 124 | } 125 | 126 | private void changed(Object arg) { 127 | this.setChanged(); 128 | this.notifyObservers(arg); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/system/AvatarHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.system; 20 | 21 | import org.kontalk.persistence.Config; 22 | import java.awt.image.BufferedImage; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | import java.util.logging.Logger; 26 | import javax.imageio.ImageIO; 27 | import org.apache.commons.codec.digest.DigestUtils; 28 | import org.kontalk.client.Client; 29 | import org.kontalk.misc.JID; 30 | import org.kontalk.model.Avatar; 31 | import org.kontalk.model.Contact; 32 | import org.kontalk.model.Model; 33 | import org.kontalk.util.MediaUtils; 34 | 35 | /** 36 | * Process incoming avatar events 37 | * @author Alexander Bikadorov {@literal } 38 | */ 39 | public final class AvatarHandler { 40 | private static final Logger LOGGER = Logger.getLogger(AvatarHandler.class.getName()); 41 | 42 | public static final List SUPPORTED_TYPES = Arrays.asList(ImageIO.getReaderMIMETypes()); 43 | 44 | private static final int MAX_SIZE = 1024 * 250; 45 | 46 | private final Client mClient; 47 | private final Model mModel; 48 | 49 | AvatarHandler(Client client, Model model) { 50 | mClient = client; 51 | mModel = model; 52 | } 53 | 54 | public void onNotify(JID jid, String id) { 55 | if (!Config.getInstance().getBoolean(Config.NET_REQUEST_AVATARS)) 56 | // disabled by user 57 | return; 58 | 59 | Contact contact = mModel.contacts().get(jid).orElse(null); 60 | if (contact == null) { 61 | LOGGER.warning("can't find contact with jid:" + jid); 62 | return; 63 | } 64 | 65 | if (id.isEmpty()) { 66 | // contact disabled avatar publishing 67 | contact.deleteAvatar(); 68 | return; 69 | } 70 | 71 | Avatar avatar = contact.getAvatar().orElse(null); 72 | if (avatar != null && avatar.getID().equals(id)) 73 | // avatar is not new 74 | return; 75 | 76 | mClient.requestAvatar(jid, id); 77 | } 78 | 79 | public void onData(JID jid, String id, byte[] avatarData) { 80 | LOGGER.info("new avatar, jid: "+jid+" id: "+id); 81 | 82 | if (avatarData.length > MAX_SIZE) 83 | LOGGER.info("avatar data too long: "+avatarData.length); 84 | 85 | Contact contact = mModel.contacts().get(jid).orElse(null); 86 | if (contact == null) { 87 | LOGGER.warning("can't find contact with jid:" + jid); 88 | return; 89 | } 90 | 91 | if (!id.equals(DigestUtils.sha1Hex(avatarData))) { 92 | LOGGER.warning("this is not what we wanted"); 93 | return; 94 | } 95 | 96 | BufferedImage img = MediaUtils.readImage(avatarData).orElse(null); 97 | if (img == null) 98 | return; 99 | 100 | contact.setAvatar(new Avatar.DefaultAvatar(id, img)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/system/ChatStateManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.system; 20 | 21 | import java.util.Map; 22 | import java.util.Timer; 23 | import java.util.TimerTask; 24 | import java.util.WeakHashMap; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | import org.jivesoftware.smackx.chatstates.ChatState; 28 | import org.kontalk.client.Client; 29 | import org.kontalk.model.Contact; 30 | import org.kontalk.model.chat.Chat; 31 | import org.kontalk.model.chat.SingleChat; 32 | import org.kontalk.persistence.Config; 33 | 34 | /** 35 | * Manager handling own chat status for all chats. 36 | * @author Alexander Bikadorov {@literal } 37 | */ 38 | final class ChatStateManager { 39 | 40 | private static final int COMPOSING_TO_PAUSED = 15; // seconds 41 | 42 | private final Client mClient; 43 | private final Map mChatStateCache = new WeakHashMap<>(); 44 | private final Timer mTimer = new Timer("Chat State Timer", true); 45 | 46 | public ChatStateManager(Client client) { 47 | mClient = client; 48 | } 49 | 50 | void handleOwnChatStateEvent(Chat chat, ChatState state) { 51 | if (!mChatStateCache.containsKey(chat)) { 52 | if (state == ChatState.gone) 53 | // weare and stay at the default state 54 | return; 55 | mChatStateCache.put(chat, new MyChatState(chat)); 56 | } 57 | 58 | mChatStateCache.get(chat).handleState(state); 59 | } 60 | 61 | void imGone() { 62 | mChatStateCache.values().forEach(chatState -> chatState.handleState(ChatState.gone)); 63 | } 64 | 65 | private class MyChatState { 66 | private final Chat mChat; 67 | private ChatState mCurrentState; 68 | private TimerTask mScheduledStateSet = null; 69 | 70 | private MyChatState(Chat chat) { 71 | mChat = chat; 72 | } 73 | 74 | private void handleState(ChatState state) { 75 | if (mScheduledStateSet != null) 76 | // whatever we wanted to set next, thats obsolete now 77 | mScheduledStateSet.cancel(); 78 | 79 | if (state != mCurrentState) 80 | this.setNewState(state); 81 | 82 | if (state == ChatState.composing) { 83 | mScheduledStateSet = new TimerTask() { 84 | @Override 85 | public void run() { 86 | // NOTE: using 'inactive' instead of 'paused' here as 87 | // 'inactive' isn't send at all 88 | MyChatState.this.handleState(ChatState.inactive); 89 | } 90 | }; 91 | mTimer.schedule(mScheduledStateSet, 92 | TimeUnit.SECONDS.toMillis(COMPOSING_TO_PAUSED)); 93 | } 94 | } 95 | 96 | private void setNewState(ChatState state) { 97 | // currently set states from XEP-0085: active, inactive, composing 98 | mCurrentState = state; 99 | 100 | if (state == ChatState.active || !(mChat instanceof SingleChat)) 101 | // don't send for groups (TODO (?)) 102 | // 'active' is send inside a message 103 | return; 104 | 105 | Contact contact = ((SingleChat) mChat).getMember().getContact(); 106 | if (contact.isMe() || contact.isBlocked() || contact.isDeleted()) 107 | return; 108 | 109 | if (Config.getInstance().getBoolean(Config.NET_SEND_CHAT_STATE)) 110 | mClient.sendChatState(contact.getJID(), mChat.getXMPPID(), state); 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/util/CryptoUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.util; 20 | 21 | import javax.crypto.Cipher; 22 | import java.lang.reflect.Field; 23 | import java.lang.reflect.Modifier; 24 | import java.security.NoSuchAlgorithmException; 25 | import java.security.Permission; 26 | import java.security.PermissionCollection; 27 | import java.util.Map; 28 | import java.util.logging.Level; 29 | import java.util.logging.Logger; 30 | 31 | /** 32 | * 33 | * @author Alexander Bikadorov {@literal } 34 | */ 35 | public class CryptoUtils { 36 | private static final Logger LOGGER = Logger.getLogger(CryptoUtils.class.getName()); 37 | 38 | /** 39 | * Ugly hack to get “unlimited strength” for the Java Encryption Extension. 40 | * Source: https://stackoverflow.com/a/22492582 41 | * And even more evil: http://stackoverflow.com/a/3301720/6286694 42 | */ 43 | public static boolean removeCryptographyRestrictions() { 44 | try { 45 | if (Cipher.getMaxAllowedKeyLength("RC5") >= 256) { 46 | LOGGER.config("cryptography restrictions removal not needed"); 47 | return true; 48 | } } catch (NoSuchAlgorithmException ex) { 49 | LOGGER.log(Level.WARNING, "can't check for crypto restriction", ex); 50 | } 51 | 52 | try { 53 | /* 54 | * Do the following, but with reflection to bypass access checks: 55 | * 56 | * JceSecurity.isRestricted = false; 57 | * JceSecurity.defaultPolicy.perms.clear(); 58 | * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE); 59 | */ 60 | Class jceSecurity = Class.forName("javax.crypto.JceSecurity"); 61 | Class cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions"); 62 | Class cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission"); 63 | 64 | Field isRestrictedField = jceSecurity.getDeclaredField("isRestricted"); 65 | isRestrictedField.setAccessible(true); 66 | 67 | // "isRestricted" field is final now, remove this 68 | Field modifiersField = Field.class.getDeclaredField("modifiers"); 69 | modifiersField.setAccessible(true); 70 | modifiersField.setInt(isRestrictedField, isRestrictedField.getModifiers() & ~Modifier.FINAL); 71 | 72 | isRestrictedField.set(null, false); 73 | 74 | Field defaultPolicyField = jceSecurity.getDeclaredField("defaultPolicy"); 75 | defaultPolicyField.setAccessible(true); 76 | PermissionCollection defaultPolicy = (PermissionCollection) defaultPolicyField.get(null); 77 | 78 | Field perms = cryptoPermissions.getDeclaredField("perms"); 79 | perms.setAccessible(true); 80 | ((Map) perms.get(defaultPolicy)).clear(); 81 | 82 | Field instance = cryptoAllPermission.getDeclaredField("INSTANCE"); 83 | instance.setAccessible(true); 84 | defaultPolicy.add((Permission) instance.get(null)); 85 | 86 | LOGGER.info("removed cryptography restrictions"); 87 | } catch (ClassNotFoundException | 88 | NoSuchFieldException | 89 | SecurityException | 90 | IllegalArgumentException | 91 | IllegalAccessException ex) { 92 | LOGGER.log(Level.WARNING, "can't remove cryptography restrictions", ex); 93 | return false; 94 | } 95 | return true; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/util/EncodingUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.util; 20 | 21 | import java.io.UnsupportedEncodingException; 22 | import java.net.URI; 23 | import java.net.URISyntaxException; 24 | import java.util.Base64; 25 | import java.util.EnumSet; 26 | import java.util.Map; 27 | import java.util.logging.Level; 28 | import java.util.logging.Logger; 29 | 30 | import org.apache.commons.lang.RandomStringUtils; 31 | import org.apache.commons.lang.StringUtils; 32 | import org.json.simple.JSONObject; 33 | 34 | public final class EncodingUtils { 35 | private static final Logger LOGGER = Logger.getLogger(EncodingUtils.class.getName()); 36 | 37 | public static final String EOL = System.getProperty("line.separator"); 38 | 39 | private EncodingUtils() { throw new AssertionError(); } 40 | 41 | /** 42 | * Get an enum set by parsing an integer which represents a bit array. 43 | * Source: http://stackoverflow.com/questions/2199399/storing-enumset-in-a-database 44 | * @param type of elements in enum set 45 | * @param enumClass enum class to determine the type 46 | * @param decoded integer decoded as 47 | * @return an enum set containing the enums specified by the integer 48 | */ 49 | public static > EnumSet intToEnumSet(Class enumClass, int decoded) { 50 | EnumSet enumSet = EnumSet.noneOf(enumClass); 51 | T[] enums = enumClass.getEnumConstants(); 52 | while (decoded != 0) { 53 | int ordinal = Integer.numberOfTrailingZeros(decoded); 54 | enumSet.add(enums[ordinal]); 55 | decoded -= Integer.lowestOneBit(decoded); 56 | } 57 | return enumSet; 58 | } 59 | 60 | /** 61 | * Encode an enum set to an integer representing a bit array. 62 | */ 63 | public static int enumSetToInt(EnumSet enumSet) { 64 | int b = 0; 65 | for (Object o : enumSet) { 66 | b += 1 << ((Enum) o).ordinal(); 67 | } 68 | return b; 69 | } 70 | 71 | // using legacy lib, raw types extend Object 72 | @SuppressWarnings("unchecked") 73 | public static void putJSON(JSONObject json, String key, String value) { 74 | if (!value.isEmpty()) 75 | json.put(key, value); 76 | } 77 | 78 | public static String getJSONString(Map map, String key) { 79 | return StringUtils.defaultString((String) map.get(key)); 80 | } 81 | 82 | public static byte[] base64ToBytes(String base64) { 83 | return Base64.getDecoder().decode(base64); 84 | } 85 | 86 | public static String bytesToBase64(byte[] bytes) { 87 | return Base64.getEncoder().encodeToString(bytes); 88 | } 89 | 90 | public static URI toURI(String str) { 91 | try { 92 | return new URI(str); 93 | } catch (URISyntaxException ex) { 94 | LOGGER.log(Level.WARNING, "invalid URI", ex); 95 | } 96 | return URI.create(""); 97 | } 98 | 99 | public static String randomString(int length) { 100 | return RandomStringUtils.randomAlphanumeric(length); 101 | } 102 | 103 | public static byte[] stringToBytes(String string) { 104 | try { 105 | return string.getBytes("utf-8"); 106 | } catch (UnsupportedEncodingException ex) { 107 | LOGGER.log(Level.WARNING, "UTF-8 not supported", ex); 108 | return string.getBytes(); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/util/MessageUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2017 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.util; 20 | 21 | import org.kontalk.model.message.OutMessage; 22 | 23 | /** 24 | * Static utilities used as interface between control, crypto and client. 25 | * @author Alexander Bikadorov {@literal } 26 | */ 27 | public class MessageUtils { 28 | 29 | private MessageUtils() {} 30 | 31 | public static class SendTask { 32 | 33 | public enum Encryption {NONE, RFC3923, XEP0373} 34 | 35 | public final OutMessage message; 36 | public final Encryption encryption; 37 | public final boolean sendChatState; 38 | 39 | private String encryptedData = ""; 40 | 41 | public SendTask(OutMessage message, Encryption encryption, boolean sendChatState) { 42 | this.message = message; 43 | this.encryption = encryption; 44 | this.sendChatState = sendChatState; 45 | } 46 | 47 | public void setEncryptedData(String encryptedData) { 48 | assert encryptedData.isEmpty(); 49 | 50 | this.encryptedData = encryptedData; 51 | } 52 | 53 | public String getEncryptedData() { 54 | return this.encryptedData; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/util/Tr.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.util; 20 | 21 | import java.net.URL; 22 | import java.util.Arrays; 23 | import java.util.HashMap; 24 | import java.util.Iterator; 25 | import java.util.List; 26 | import java.util.Locale; 27 | import java.util.Map; 28 | import java.util.logging.Level; 29 | import java.util.logging.Logger; 30 | import org.apache.commons.configuration.ConfigurationException; 31 | import org.apache.commons.configuration.PropertiesConfiguration; 32 | 33 | /** 34 | * Translation for strings used in view. 35 | * Use the Python script for updating the string properties file! 36 | * 37 | * @author Alexander Bikadorov {@literal } 38 | */ 39 | public class Tr { 40 | private static final Logger LOGGER = Logger.getLogger(Tr.class.getName()); 41 | 42 | private static final String DEFAULT_LANG = "en"; 43 | private static final String I18N_DIR = "i18n/"; 44 | private static final String STRING_FILE = "strings"; 45 | private static final String PROP_EXT = ".properties"; 46 | 47 | private static final String WIKI_BASE = "https://github.com/kontalk/desktopclient-java/wiki"; 48 | private static final String WIKI_HOME = "Home"; 49 | private static final List WIKI_LANGS = Arrays.asList("de"); 50 | 51 | /** Map default (English) strings to translated strings. **/ 52 | private static Map TR_MAP = null; 53 | 54 | /** 55 | * Translate string used in user interface. 56 | * Spaces at beginning or end of string not supported! 57 | * @param s string that wants to be translated (in English) 58 | * @return translation of input string (depending on platform language) 59 | */ 60 | public static String tr(String s) { 61 | if (TR_MAP == null || !TR_MAP.containsKey(s)) 62 | return s; 63 | return TR_MAP.get(s); 64 | } 65 | 66 | public static void init() { 67 | // get language 68 | String lang = Locale.getDefault().getLanguage(); 69 | // for testing 70 | //String lang = new Locale("zh").getLanguage(); 71 | if (lang.equals(DEFAULT_LANG)) { 72 | return; 73 | } 74 | 75 | LOGGER.info("Setting language: "+lang); 76 | 77 | // load string keys file 78 | String path = I18N_DIR + STRING_FILE + PROP_EXT; 79 | PropertiesConfiguration stringKeys; 80 | try { 81 | stringKeys = new PropertiesConfiguration(ClassLoader.getSystemResource(path)); 82 | } catch (ConfigurationException ex) { 83 | LOGGER.log(Level.WARNING, "can't load string key file", ex); 84 | return; 85 | } 86 | 87 | // load translation file 88 | path = I18N_DIR + STRING_FILE + "_" + lang + PROP_EXT; 89 | URL url = ClassLoader.getSystemResource(path); 90 | if (url == null) { 91 | LOGGER.info("can't find translation file: "+path); 92 | return; 93 | } 94 | PropertiesConfiguration tr = new PropertiesConfiguration(); 95 | tr.setEncoding("UTF-8"); 96 | try { 97 | tr.load(url); 98 | } catch (ConfigurationException ex) { 99 | LOGGER.log(Level.WARNING, "can't load translation file", ex); 100 | return; 101 | } 102 | 103 | TR_MAP = new HashMap<>(); 104 | Iterator it = tr.getKeys(); 105 | while (it.hasNext()) { 106 | String k = it.next(); 107 | if (!stringKeys.containsKey(k)) { 108 | LOGGER.warning("key in translation but not in key file: "+k); 109 | continue; 110 | } 111 | TR_MAP.put(stringKeys.getString(k), tr.getString(k)); 112 | } 113 | } 114 | 115 | public static String getLocalizedWikiLink() { 116 | String lang = Locale.getDefault().getLanguage(); 117 | if (WIKI_LANGS.contains(lang)) { 118 | // damn URI decoding 119 | return WIKI_BASE + "/%5B" + lang + "%5D-" + WIKI_HOME; 120 | } 121 | return WIKI_BASE; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/util/XMPPUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package org.kontalk.util; 19 | 20 | import java.util.Arrays; 21 | import java.util.List; 22 | 23 | import com.google.i18n.phonenumbers.NumberParseException; 24 | import com.google.i18n.phonenumbers.PhoneNumberUtil; 25 | import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 26 | import org.apache.commons.codec.digest.DigestUtils; 27 | import org.kontalk.misc.JID; 28 | 29 | /** 30 | * XMPP related functions. 31 | * 32 | * @author Alexander Bikadorov 33 | */ 34 | public final class XMPPUtils { 35 | 36 | // TODO do not hardcode, maybe download 37 | private static final List KONTALK_DOMAINS = Arrays.asList("beta.kontalk.net"); 38 | 39 | private XMPPUtils() { 40 | throw new AssertionError(); 41 | } 42 | 43 | public static String phoneNumberToKontalkLocal(String number) { 44 | PhoneNumberUtil pnUtil = PhoneNumberUtil.getInstance(); 45 | PhoneNumber n; 46 | try { 47 | n = pnUtil.parse(number, null); 48 | } catch (NumberParseException ex) { 49 | return ""; 50 | } 51 | 52 | if (!pnUtil.isValidNumber(n)) 53 | return ""; 54 | 55 | return DigestUtils.sha1Hex( 56 | PhoneNumberUtil.getInstance().format(n, 57 | PhoneNumberUtil.PhoneNumberFormat.E164)); 58 | } 59 | 60 | public static boolean isKontalkJID(JID jid) { 61 | return KONTALK_DOMAINS.contains(jid.domain()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/view/Content.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.view; 20 | 21 | import java.awt.BorderLayout; 22 | import java.awt.Component; 23 | import java.awt.GridBagLayout; 24 | import java.awt.event.FocusAdapter; 25 | import java.awt.event.FocusEvent; 26 | 27 | import com.alee.laf.label.WebLabel; 28 | import com.alee.laf.panel.WebPanel; 29 | import org.kontalk.model.Contact; 30 | import org.kontalk.model.chat.Chat; 31 | 32 | /** 33 | * Content view area: show a chat or contact details 34 | * @author Alexander Bikadorov {@literal } 35 | */ 36 | final class Content extends WebPanel { 37 | 38 | private final View mView; 39 | private final ChatView mChatView; 40 | private Component mCurrent; 41 | 42 | Content(View view, ChatView chatView) { 43 | mView = view; 44 | mChatView = chatView; 45 | 46 | this.addFocusListener(new FocusAdapter() { 47 | @Override 48 | public void focusGained(FocusEvent e) { 49 | // maybe not visible, i don't care 50 | mChatView.requestFocusInWindow(); 51 | } 52 | }); 53 | 54 | this.show(mChatView); 55 | } 56 | 57 | void showChat(Chat chat) { 58 | mChatView.showChat(chat); 59 | if (mCurrent != mChatView) 60 | this.show(mChatView); 61 | } 62 | 63 | void showContact(Contact contact) { 64 | this.show(ContactDetails.instance(mView, contact)); 65 | } 66 | 67 | void showNothing() { 68 | WebPanel nothing = new WebPanel(); 69 | WebPanel topPanel = new WebPanel(new GridBagLayout()); 70 | topPanel.setMargin(40); 71 | topPanel.add(new WebLabel(Utils.getIcon("kontalk-big.png"))); 72 | nothing.add(topPanel, BorderLayout.NORTH); 73 | this.show(nothing); 74 | } 75 | 76 | private void show(Component comp) { 77 | // Swing... 78 | this.removeAll(); 79 | this.add(comp, BorderLayout.CENTER); 80 | this.revalidate(); 81 | this.repaint(); 82 | 83 | mCurrent = comp; 84 | } 85 | 86 | void requestRenameFocus() { 87 | if (mCurrent instanceof ContactDetails) { 88 | ((ContactDetails) mCurrent).setRenameFocus(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/view/ImageLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.view; 20 | 21 | import java.nio.file.Path; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import javax.swing.ImageIcon; 25 | import org.kontalk.system.AttachmentManager; 26 | import org.kontalk.util.MediaUtils; 27 | 28 | /** 29 | * Static utility functions for loading images in Swing. 30 | * @author Alexander Bikadorov {@literal } 31 | */ 32 | class ImageLoader { 33 | 34 | private static final Map CACHE = new HashMap<>(); 35 | 36 | private ImageLoader() {} 37 | 38 | static ImageIcon imageIcon(Path path) { 39 | if (CACHE.containsKey(path)) 40 | return CACHE.get(path); 41 | 42 | ImageIcon imageIcon = load(path); 43 | CACHE.put(path, imageIcon); 44 | return imageIcon; 45 | } 46 | 47 | private static ImageIcon load(Path path) { 48 | return new ImageIcon( 49 | MediaUtils.scale( 50 | MediaUtils.readImage(path), 51 | AttachmentManager.THUMBNAIL_DIM.width, 52 | AttachmentManager.THUMBNAIL_DIM.height)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/view/LinkUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package org.kontalk.view; 19 | 20 | import javax.swing.event.MouseInputAdapter; 21 | import javax.swing.text.BadLocationException; 22 | import javax.swing.text.Element; 23 | import javax.swing.text.Style; 24 | import javax.swing.text.StyleConstants; 25 | import javax.swing.text.StyleContext; 26 | import javax.swing.text.StyledDocument; 27 | import java.awt.Color; 28 | import java.awt.Cursor; 29 | import java.awt.event.MouseAdapter; 30 | import java.awt.event.MouseEvent; 31 | import java.net.MalformedURLException; 32 | import java.net.URL; 33 | import java.util.logging.Level; 34 | import java.util.logging.Logger; 35 | import java.util.regex.Matcher; 36 | import java.util.regex.Pattern; 37 | 38 | import com.alee.laf.text.WebTextPane; 39 | import com.alee.utils.WebUtils; 40 | 41 | /** 42 | * Static methods/field for parsing web links in the text of a WebTextPane. 43 | * Cause Android has Linkify and I have to write this myself .( 44 | * Some parts taken from: https://community.oracle.com/thread/2089990 45 | * 46 | * @author Alexander Bikadorov {@literal } 47 | */ 48 | final class LinkUtils { 49 | private static final Logger LOGGER = Logger.getLogger(LinkUtils.class.getName()); 50 | 51 | static final TextClickListener CLICK_LISTENER = new TextClickListener(); 52 | static final TextMotionListener MOTION_LISTENER = new TextMotionListener(); 53 | 54 | private static final Style DEFAULT_STYLE 55 | = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE); 56 | /** Undoubtedly the best URL regex ever made. */ 57 | private static final Pattern URL_PATTERN = Pattern.compile( 58 | "(http[s]?://)?" + // scheme; group 1 59 | "(\\w+[a-zA-Z_0-9-]*\\w+\\.)+" + // sub- and host-level(s); group 2 60 | "[a-z]{2,}(:[0-9]+)?" + // TLD and port; group 3 61 | "(/[^\\s?#/]*)*" + // path; group 4 62 | "(\\?[^\\s?#]*)*" + // query; group 5 63 | "(\\#[^\\s?#]*)*", // fragment; group 6 64 | Pattern.CASE_INSENSITIVE); 65 | 66 | private static final String URL_ATT_NAME = "URL"; 67 | 68 | static class Linkifier { 69 | 70 | private final StyledDocument mDocument; 71 | // style for all links in a document 72 | private final Style mURLStyle; 73 | 74 | Linkifier(StyledDocument doc) { 75 | mDocument = doc; 76 | 77 | mURLStyle = mDocument.addStyle(null, DEFAULT_STYLE); 78 | StyleConstants.setForeground(mURLStyle, Color.BLUE); 79 | // only for identifying URLs 80 | mURLStyle.addAttribute(URL_ATT_NAME, new Object()); 81 | } 82 | 83 | void linkify(String text) throws BadLocationException { 84 | Matcher m = URL_PATTERN.matcher(text); 85 | int lastPos = 0; 86 | while (m.find()) { 87 | // non-matching 88 | insertDefault(mDocument, text.substring(lastPos, m.start())); 89 | // matching 90 | mDocument.insertString(mDocument.getLength(), m.group(), mURLStyle); 91 | lastPos = m.end(); 92 | } 93 | // last non-matching 94 | insertDefault(mDocument, lastPos >= text.length() ? "" : text.substring(lastPos)); 95 | } 96 | } 97 | 98 | private static void insertDefault(StyledDocument doc, String text) 99 | throws BadLocationException { 100 | doc.insertString(doc.getLength(), text, DEFAULT_STYLE); 101 | } 102 | 103 | private static class TextClickListener extends MouseAdapter { 104 | @Override 105 | public void mouseClicked(MouseEvent e) { 106 | WebTextPane textPane = (WebTextPane) e.getComponent(); 107 | StyledDocument doc = textPane.getStyledDocument(); 108 | Element elem = doc.getCharacterElement(textPane.viewToModel(e.getPoint())); 109 | if (!elem.getAttributes().isDefined(URL_ATT_NAME)) 110 | // not a link 111 | return; 112 | 113 | int len = elem.getEndOffset() - elem.getStartOffset(); 114 | final String url; 115 | try { 116 | url = doc.getText(elem.getStartOffset(), len); 117 | } catch (BadLocationException ex) { 118 | LOGGER.log(Level.WARNING, "can't get URL", ex); 119 | return; 120 | } 121 | 122 | Runnable run = new Runnable() { 123 | @Override 124 | public void run() { 125 | WebUtils.browseSiteSafely(fixProto(url)); 126 | } 127 | }; 128 | new Thread(run, "Link Browser").start(); 129 | } 130 | } 131 | 132 | private static class TextMotionListener extends MouseInputAdapter { 133 | @Override 134 | public void mouseMoved(MouseEvent e) { 135 | WebTextPane textPane = (WebTextPane) e.getComponent(); 136 | StyledDocument doc = textPane.getStyledDocument(); 137 | Element elem = doc.getCharacterElement(textPane.viewToModel(e.getPoint())); 138 | if (elem.getAttributes().isDefined(URL_ATT_NAME)) { 139 | textPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 140 | } else { 141 | textPane.setCursor(Cursor.getDefaultCursor()); 142 | } 143 | } 144 | } 145 | 146 | private static String fixProto(String url) { 147 | try { 148 | new URL(url); 149 | return url; 150 | } catch (MalformedURLException ignored) { 151 | } 152 | url = "http://" + url; 153 | try { 154 | new URL(url); 155 | } catch (MalformedURLException ex) { 156 | LOGGER.log(Level.WARNING, "invalid url", ex); 157 | } 158 | return url; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/view/ObserverTrait.java: -------------------------------------------------------------------------------- 1 | package org.kontalk.view; 2 | 3 | import javax.swing.SwingUtilities; 4 | import java.util.Observable; 5 | import java.util.Observer; 6 | 7 | /** 8 | * @author Alexander Bikadorov {@literal } 9 | */ 10 | interface ObserverTrait extends Observer { 11 | 12 | @Override 13 | default void update(Observable o, Object arg) { 14 | if (SwingUtilities.isEventDispatchThread()) { 15 | this.updateOnEDT(o, arg); 16 | return; 17 | } 18 | SwingUtilities.invokeLater(() -> ObserverTrait.this.updateOnEDT(o, arg)); 19 | } 20 | 21 | void updateOnEDT(Observable o, Object arg); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/view/SearchPanel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.view; 20 | 21 | import javax.swing.Icon; 22 | import javax.swing.event.DocumentEvent; 23 | import javax.swing.event.DocumentListener; 24 | import java.awt.BorderLayout; 25 | import java.awt.event.ActionEvent; 26 | import java.awt.event.ActionListener; 27 | 28 | import com.alee.extended.image.WebImage; 29 | import com.alee.laf.button.WebButton; 30 | import com.alee.laf.panel.WebPanel; 31 | import com.alee.laf.text.WebTextField; 32 | import org.kontalk.util.Tr; 33 | 34 | /** 35 | * A search bar to search for text in contact, chat and message lists. 36 | * @author Alexander Bikadorov {@literal } 37 | */ 38 | final class SearchPanel extends WebPanel { 39 | private final WebTextField mSearchField; 40 | 41 | SearchPanel(final ListView[] lists, final ChatView chatView) { 42 | mSearchField = new WebTextField(); 43 | mSearchField.setInputPrompt(Tr.tr("Search…")); 44 | mSearchField.getDocument().addDocumentListener(new DocumentListener() { 45 | @Override 46 | public void insertUpdate(DocumentEvent e) { 47 | this.filterList(); 48 | } 49 | @Override 50 | public void removeUpdate(DocumentEvent e) { 51 | this.filterList(); 52 | } 53 | @Override 54 | public void changedUpdate(DocumentEvent e) { 55 | this.filterList(); 56 | } 57 | private void filterList() { 58 | String searchText = mSearchField.getText().toLowerCase(); 59 | for (ListView list : lists) 60 | list.filterItems(searchText); 61 | chatView.filterCurrentChat(searchText); 62 | } 63 | }); 64 | mSearchField.setLeadingComponent(new WebImage(Utils.getIcon("ic_ui_search.png"))); 65 | Icon clearIcon = Utils.getIcon("ic_ui_clear.png"); 66 | WebButton clearSearchButton = new WebButton(clearIcon); 67 | clearSearchButton.setUndecorated(true); 68 | clearSearchButton.addActionListener(new ActionListener() { 69 | @Override 70 | public void actionPerformed(ActionEvent e) { 71 | mSearchField.clear(); 72 | } 73 | }); 74 | mSearchField.setTrailingComponent(clearSearchButton); 75 | this.add(mSearchField, BorderLayout.CENTER); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/kontalk/view/TrayManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kontalk.view; 20 | 21 | import java.awt.AWTException; 22 | import java.awt.Image; 23 | import java.awt.SystemTray; 24 | import java.awt.TrayIcon; 25 | import java.awt.event.ActionEvent; 26 | import java.awt.event.ActionListener; 27 | import java.awt.event.MouseAdapter; 28 | import java.awt.event.MouseEvent; 29 | import java.awt.event.MouseListener; 30 | import java.util.Observable; 31 | import java.util.logging.Level; 32 | import java.util.logging.Logger; 33 | 34 | import com.alee.laf.menu.WebMenuItem; 35 | import com.alee.laf.menu.WebPopupMenu; 36 | import com.alee.laf.rootpane.WebDialog; 37 | import org.kontalk.model.Model; 38 | import org.kontalk.model.chat.ChatList; 39 | import org.kontalk.persistence.Config; 40 | import org.kontalk.util.Tr; 41 | 42 | /** 43 | * 44 | * @author Alexander Bikadorov {@literal } 45 | */ 46 | final class TrayManager implements ObserverTrait { 47 | private static final Logger LOGGER = Logger.getLogger(TrayManager.class.getName()); 48 | 49 | private static final Image NORMAL_TRAY = Utils.getImage("kontalk.png"); 50 | private static final Image NOTIFICATION_TRAY = Utils.getImage("kontalk_notification.png"); 51 | 52 | private final View mView; 53 | private final Model mModel; 54 | private final MainFrame mMainFrame; 55 | private TrayIcon mTrayIcon = null; 56 | 57 | TrayManager(View view, Model model, MainFrame mainFrame) { 58 | mView = view; 59 | mModel = model; 60 | mMainFrame = mainFrame; 61 | this.setTray(); 62 | } 63 | 64 | void setTray() { 65 | if (!Config.getInstance().getBoolean(Config.MAIN_TRAY)) { 66 | this.removeTray(); 67 | return; 68 | } 69 | 70 | if (!SystemTray.isSupported()) { 71 | LOGGER.info("tray icon not supported"); 72 | return; 73 | } 74 | 75 | if (mTrayIcon == null) 76 | mTrayIcon = this.createTrayIcon(); 77 | 78 | SystemTray tray = SystemTray.getSystemTray(); 79 | if (tray.getTrayIcons().length > 0) 80 | return; 81 | 82 | try { 83 | tray.add(mTrayIcon); 84 | } catch (AWTException ex) { 85 | LOGGER.log(Level.WARNING, "can't add tray icon", ex); 86 | } 87 | } 88 | 89 | void removeTray() { 90 | if (mTrayIcon != null) { 91 | SystemTray tray = SystemTray.getSystemTray(); 92 | tray.remove(mTrayIcon); 93 | } 94 | } 95 | 96 | @Override 97 | public void updateOnEDT(Observable o, Object arg) { 98 | if (arg != ChatList.ViewChange.UNREAD) 99 | return; 100 | 101 | if (mTrayIcon == null) 102 | return; 103 | 104 | mTrayIcon.setImage(getTrayImage()); 105 | } 106 | 107 | private Image getTrayImage() { 108 | return mModel.chats().isUnread() ? 109 | NOTIFICATION_TRAY : 110 | NORMAL_TRAY ; 111 | } 112 | 113 | private TrayIcon createTrayIcon() { 114 | // popup menu outside of frame, officially not supported 115 | final WebPopupMenu popup = new WebPopupMenu(); 116 | WebMenuItem quitItem = new WebMenuItem(Tr.tr("Quit")); 117 | quitItem.addActionListener(new ActionListener() { 118 | @Override 119 | public void actionPerformed(ActionEvent event) { 120 | mView.callShutDown(); 121 | } 122 | }); 123 | popup.add(quitItem); 124 | 125 | // workaround: menu does not disappear when focus is lost 126 | final WebDialog hiddenDialog = new WebDialog(); 127 | hiddenDialog.setUndecorated(true); 128 | 129 | // create an action listener to listen for default action executed on the tray icon 130 | MouseListener listener = new MouseAdapter() { 131 | @Override 132 | public void mousePressed(MouseEvent e) { 133 | // menu must be shown on mouse release 134 | //check(e); 135 | } 136 | @Override 137 | public void mouseReleased(MouseEvent e) { 138 | if (e.getButton() == MouseEvent.BUTTON1) 139 | mMainFrame.toggleState(); 140 | else 141 | check(e); 142 | } 143 | private void check(MouseEvent e) { 144 | // if (!e.isPopupTrigger()) 145 | // return; 146 | 147 | hiddenDialog.setVisible(true); 148 | 149 | // TODO ugly code 150 | popup.setLocation(e.getX() - 20, e.getY() - 40); 151 | popup.setInvoker(hiddenDialog); 152 | popup.setCornerWidth(0); 153 | popup.setVisible(true); 154 | } 155 | }; 156 | 157 | TrayIcon trayIcon = new TrayIcon(this.getTrayImage(), "Kontalk" /*, popup*/); 158 | trayIcon.setImageAutoSize(true); 159 | trayIcon.addMouseListener(listener); 160 | return trayIcon; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/resources/i18n/strings_eu.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/i18n/strings_eu.properties -------------------------------------------------------------------------------- /src/main/resources/i18n/strings_fi.properties: -------------------------------------------------------------------------------- 1 | s1=Valinnat 2 | s2=Ohje 3 | s_JSQ2=Lähetä 4 | s_NMPC=Lähetä viesti 5 | s_8WWE=Poistu 6 | s_6P7W=Yhdistetään... 7 | s_U0MG=Yhdistetty 8 | s_769Q=Katkaistaan yhteyttä... 9 | s_B56J=Ei yhdistetty 10 | s_J1DQ=Yhdistäminen epäonnistui 11 | s_EON8=Yhdistämisvirhe 12 | s_0N2Z=Virhe 13 | s_BTPA=Salausvirhe 14 | s_CEMO=Virhe salauksen purussa 15 | s_HTYQ=Tuntematon virhe 16 | s_M63K=Tuntematon virhe!? 17 | s_OW1R=Avaintiedostoja ei voida kirjoittaa asetushakemistoon. 18 | s_LHST=Avaintiedostojen luku asetushakemistosta ei onnistu. 19 | s_6D91=Ole hyvä ja tuo avaimesi uudelleen. 20 | s_K94P=Yhteyden muodostaminen ei onnistu 21 | s_HZPW=Palvelimeen ei voida yhdistää. 22 | s_J8KE=Palvelin hylkää avaimen. 23 | s_H4GJ=Palvelin ei vastaa. 24 | s_HLM5=Javan versio ei ole tuettu 25 | -------------------------------------------------------------------------------- /src/main/resources/i18n/strings_in.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/i18n/strings_in.properties -------------------------------------------------------------------------------- /src/main/resources/i18n/strings_sr.properties: -------------------------------------------------------------------------------- 1 | s1=Опције 2 | s2=Помоћ 3 | s_JSQ2=Пошаљи 4 | s_NMPC=Пошаљи поруку 5 | s_8WWE=Напусти 6 | s_6P7W=Повезујем се 7 | s_U0MG=Повезан 8 | s_769Q=Искључујем се... 9 | s_B56J=Неповезан 10 | s_J1DQ=Неуспело повезивање 11 | s_EON8=Грешка при повезивању 12 | s_0N2Z=Грешка 13 | s_BTPA=Грешка шифровања 14 | s_CEMO=Грешка дешифровања 15 | s_HTYQ=Непозната грешка 16 | s_PD0A=Кључ за примаоца није нађен. 17 | s_125B=Неуобичајена грешка кодера 18 | s_M63K=Непозната грешка!? 19 | s_S39E=Не могу да учитам фајл кључа из архиве. 20 | s_VYCJ=Не могу да направим лични кључ из фајлова. 21 | s_RQHM=Да ли је јавни кључ важећи? 22 | s_WOYH=Да ли су сви кључеви важећи? 23 | s_53Z5=Да ли је лозинка исправна? 24 | s_Q4J7=Не могу да променим лозинку. Интерна грешка(!?) 25 | s_OW1R=Не могу да упишем фајлове кључа у директоријум подешавања. 26 | s_LHST=Не могу да читам фајлове кључа из директоријума подешавања. 27 | s_C9CO=Не могу да учитам фајлове кључа из директоријума подешавања. 28 | s_6D91=Поново увезите ваш кључ. 29 | s_K94P=Не могу да направим везу 30 | s_HZPW=Не могу да се повежем са сервером. 31 | s_QL8R=Да ли је адреса сервера тачна? 32 | s_J8KE=Сервер одбија кључ. 33 | s_H4GJ=Сервер не одговара. 34 | s_PVI0=Не могу да се пријавим. 35 | s_L1DT=Сервер одбија овај налог. Да ли је сервер исправно наведен и налог важећи? 36 | s_X4A9=Веза је затворена уз грешку. 37 | s_0QJ9=Инсталирана верзија Јаве је превише стара 38 | s_N0DZ=Инсталирајте Јаву 8. 39 | s_HLM5=Неподржана верзија Јаве 40 | s_8FB5=Не могу да отворим архиву кључа. 41 | s_VTOO=Повежи се 42 | s_J8B9=Повежи се на сервер 43 | s_2939=Прекини везу 44 | s_XW05=Прекини везу са сервером 45 | s_RYMX=Постави статус 46 | s_66AA=Поставите поруку стања коју шаљете кориснику 47 | s_AN83=Изађи 48 | s_NLZ0=Излази из апликације 49 | s_RDGN=Подешавања 50 | s_9UNW=Подесите апликацију 51 | s_TKHF=О програму 52 | s_4P6T=О програму 53 | s_X4K8=Ново 54 | s_7MO0=Нема разговора за приказ. Можете започети нов разговор из контаката 55 | # s_B8XU = Threads 56 | # s_012T = Add 57 | # s_VA0W = No contacts to display. You have no friends ;( 58 | s_KBMH=Контакти 59 | # s_6COL = Visit kontalk.org 60 | # s_LPY9 = Notification sound by 61 | # s_M91R = About 62 | # s_NMNS = Status 63 | # s_4QK9 = Your current status\: 64 | # s_ZVBN = Previously used\: 65 | # s_ER26 = Cancel 66 | # s_BIF7 = Save 67 | # s_XK4W = Add New Contact 68 | # s_MIAO = Display Name\: 69 | # s_YTBQ = Encryption 70 | # s_U1UW = Cancel 71 | # s_IBU6 = Save 72 | # s_S8GI = Search... 73 | # s_6RRX = Import Wizard 74 | # s_QXUP = Back 75 | # s_1GYL = Next 76 | # s_WW4U = Finish 77 | # s_UK2S = Success\! 78 | # s_E6EK = Import process finished with\: 79 | # s_M4W3 = Error description\: 80 | # s_44NP = Get Started 81 | # s_VKI8 = Welcome to the import wizard. 82 | # s_TMHY = To use the Kontalk desktop client you need an existing account. 83 | # s_EWJ4 = Please export the key files from your Android device and select them on the next page. 84 | # s_JFS8 = Setup 85 | # s_EKOL = Zip archive containing personal key\: 86 | # s_8HBO = Decryption password for key\: 87 | # s_BWQV = Enter password... 88 | # s_16WW = Show password 89 | # s_GR2F = Zip archive 90 | # s_U6NZ = Import results 91 | # s_RHOQ = Main 92 | # s_XSQM = Account 93 | # s_FVE4 = Privacy 94 | # s_T1AI = Main Settings 95 | # s_WFDO = Connect on startup 96 | # s_GCYS = Show tray icon 97 | # s_V6FV = Close to tray 98 | # s_731N = Enter key sends 99 | # s_6G56 = Enter key sends text, Control+Enter adds new line - or vice versa 100 | # s_4ORM = Custom background\: 101 | # s_H4JO = Account Configuration 102 | # s_BGK0 = Server address\: 103 | # s_6OD2 = Port\: 104 | # s_4WQX = Disable certificate validation 105 | # s_VMP3 = Disable SSL certificate server validation 106 | # s_Y7G0 = Import new Account 107 | # s_9FKT = Save & Connect 108 | # s_X97A = no key loaded 109 | # s_3Q3G = Key fingerprint\: 110 | # s_84MW = Privacy Settings 111 | # s_UP85 = Send chatstate notification 112 | # s_MGHZ = 113 | # s_CYIK = No 114 | # s_9NTE = ? 115 | # s_1SED = Yes 116 | # s_D0BD = YES 117 | # s_3OK6 = Available 118 | # s_G3C7 = Blocked 119 | # s_956V = Last seen 120 | # s_PDYT = New Thread 121 | # s_052Y = Creates a new thread for this contact 122 | # s_OUKY = Edit Contact 123 | # s_AOG4 = Edit this contact 124 | # s_9KDJ = Block Contact 125 | # s_FF9R = Block all messages from this contact 126 | # s_AEX9 = Unblock Contact 127 | # s_QU3S = Unblock this contact 128 | # s_J57E = Delete Contact 129 | # s_AHPF = Delete this contact 130 | # s_8GRW = Edit Contact 131 | # s_45P5 = Encryption Key 132 | # s_AW5R = Available 133 | # s_C0EF = Not Available 134 | # s_X9O1 = Changing the JID is only useful in very rare cases. Are you sure? 135 | # s_HEJA = Please Confirm 136 | # s_ELUO = Edit Thread 137 | # s_4ZBG = Edit this thread 138 | # s_HAK6 = Delete Thread 139 | # s_MD21 = Delete this thread 140 | # s_ZKM0 = Please Confirm 141 | # s_CCNS = no messages yet 142 | # s_FSGV = Last activity 143 | # s_Z35B = 144 | # s_D7B3 = 145 | # s_O85V = Edit Thread 146 | # s_AKL2 = Subject\: 147 | # s_D2D8 = Participants\: 148 | # s_UEN4 = More than one receiver not supported (yet). 149 | # s_LQ9X = Sorry 150 | # s_4LCO = [encrypted] 151 | # s_92KK = ? 152 | # s_OEGV = Attachment\: 153 | # s_BUP5 = Decrypt 154 | # s_7770 = Retry decrypting message 155 | # s_5RDK = Copy 156 | # s_0R7Z = Copy message content 157 | # s_6AKS = unknown 158 | # s_YNOC = not encrypted 159 | # s_7BO5 = encrypted 160 | # s_ZN6V = decrypted 161 | # s_WVG1 = unknown 162 | # s_6OKN = not signed 163 | # s_A812 = signed 164 | # s_CAM4 = verified 165 | # s_4N5N = none 166 | # s_YV5S = Security 167 | # s_QNRV = Problems 168 | # s_IGQC = Permanently delete all messages in this thread? 169 | # s_V5R1 = Custom Background 170 | # s_URN0 = Color\: 171 | # s_X171 = Image\: 172 | # s_T3V1 = loading... 173 | # s_H3ZD = downloading... 174 | # s_44X2 = download failed 175 | 176 | s_6COL=Посетите kontalk.org 177 | s_NMNS=Стање 178 | s_4QK9=Ваше текуће стање: 179 | s_ZVBN=Претходно кориштено: 180 | s_ER26=Одустани 181 | s_BIF7=Сачувај 182 | s_YTBQ=Шифровање 183 | s_QXUP=Назад 184 | s_1GYL=Следеће 185 | s_UK2S=Успех! 186 | s_M4W3=Опис грешке: 187 | s_44NP=Почнимо 188 | s_16WW=Прикажи лозинку 189 | s_XSQM=Налог 190 | s_RHOQ=Главно 191 | s_FVE4=Приватност 192 | s_T1AI=Главне поставке 193 | s_BGK0=Адреса сервера: 194 | s_6OD2=Порт: 195 | s_84MW=Поставке приватности 196 | s_URN0=Боја: 197 | s_X171=Слика: 198 | s_44X2=преузимање није успело 199 | s_WDMP=Отисак: 200 | s_HKW8=Завршено 201 | s_P9J2=Састављена: 202 | s_Y79T=Испоручена: 203 | s_ZVJJ=Примљена: 204 | -------------------------------------------------------------------------------- /src/main/resources/img/chat_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/chat_bg.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_crypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_crypt.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_crypt_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_crypt_warning.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_delivered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_delivered.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_error.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_pending.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_sent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_sent.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_unencrypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_unencrypt.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_msg_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_msg_warning.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_add_contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_add_contact.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_add_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_add_group.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_attach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_attach.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_clear.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_edit.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_insecure.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_menu.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_refresh.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_reload.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_search.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_secure.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_send.png -------------------------------------------------------------------------------- /src/main/resources/img/ic_ui_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/ic_ui_warning.png -------------------------------------------------------------------------------- /src/main/resources/img/kontalk-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/kontalk-big.png -------------------------------------------------------------------------------- /src/main/resources/img/kontalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/kontalk.png -------------------------------------------------------------------------------- /src/main/resources/img/kontalk_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/kontalk_notification.png -------------------------------------------------------------------------------- /src/main/resources/img/kontalk_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/img/kontalk_small.png -------------------------------------------------------------------------------- /src/main/resources/notification.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/notification.ogg -------------------------------------------------------------------------------- /src/main/resources/truststore.bks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/src/main/resources/truststore.bks -------------------------------------------------------------------------------- /src/test/java/org/kontalk/KontalkTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package org.kontalk; 19 | 20 | import java.io.IOException; 21 | import java.nio.file.Path; 22 | import org.junit.After; 23 | import org.junit.AfterClass; 24 | import org.junit.Before; 25 | import org.junit.BeforeClass; 26 | import org.junit.ClassRule; 27 | import org.junit.Test; 28 | import org.junit.rules.TemporaryFolder; 29 | import static org.junit.Assert.assertEquals; 30 | import static org.junit.Assert.fail; 31 | import org.junit.Ignore; 32 | 33 | /** 34 | * 35 | * @author Alexander Bikadorov {@literal } 36 | */ 37 | public class KontalkTest { 38 | @ClassRule 39 | public static TemporaryFolder TEMP_FOLDER = new TemporaryFolder(); 40 | 41 | private static Path APP_DIR; 42 | 43 | public KontalkTest() { 44 | } 45 | 46 | @BeforeClass 47 | public static void setUpClass() { 48 | APP_DIR = TEMP_FOLDER.getRoot().toPath().resolve("app_dir"); 49 | } 50 | 51 | @AfterClass 52 | public static void tearDownClass() throws IOException { 53 | } 54 | 55 | @Before 56 | public void setUp() { 57 | } 58 | 59 | @After 60 | public void tearDown() { 61 | } 62 | 63 | /** 64 | * Test of start method, of class Kontalk. 65 | */ 66 | @Test 67 | public void testStart() { 68 | System.out.println("start"); 69 | Kontalk app = new Kontalk(APP_DIR); 70 | int returnCode = app.start(false); 71 | assertEquals(returnCode, 0); 72 | // TODO stop 73 | } 74 | 75 | /** 76 | * Test of main method, of class Kontalk. 77 | */ 78 | @Test 79 | @Ignore 80 | public void testMain() { 81 | System.out.println("main"); 82 | String[] args = null; 83 | Kontalk.main(args); 84 | // TODO review the generated test code and remove the default call to fail. 85 | fail("The test case is a prototype."); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/org/kontalk/util/CryptoUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Kontalk Java client 3 | * Copyright (C) 2016 Kontalk Devteam 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package org.kontalk.util; 19 | 20 | import org.junit.After; 21 | import org.junit.AfterClass; 22 | import org.junit.Before; 23 | import org.junit.BeforeClass; 24 | import org.junit.Test; 25 | 26 | /** 27 | * 28 | * @author Alexander Bikadorov {@literal } 29 | */ 30 | public class CryptoUtilsTest { 31 | 32 | public CryptoUtilsTest() { 33 | } 34 | 35 | @BeforeClass 36 | public static void setUpClass() { 37 | } 38 | 39 | @AfterClass 40 | public static void tearDownClass() { 41 | } 42 | 43 | @Before 44 | public void setUp() { 45 | } 46 | 47 | @After 48 | public void tearDown() { 49 | } 50 | 51 | /** 52 | * Test of removeCryptographyRestrictions method, of class CryptoUtils. 53 | * NOTE: crypto restriction removal is platform dependend, so is the 54 | * result of this test 55 | */ 56 | @Test 57 | public void testRemoveCryptographyRestrictions() { 58 | System.out.println("removeCryptographyRestrictions"); 59 | boolean succ = CryptoUtils.removeCryptographyRestrictions(); 60 | assert (succ); 61 | // TODO better test by trying out 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /update_tr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Created on Fri Mar 13 14:20:20 2015 6 | 7 | Kontalk Java client 8 | Copyright (C) 2014 Kontalk Devteam 9 | 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | 24 | Find all translation strings in Java code and update the strings property file. 25 | 26 | Usage: $python update_tr.py src/main/resources/res/i18n/strings.properties src/main/java/org/kontalk/view 27 | 28 | @author: Alexander Bikadorov 29 | """ 30 | 31 | import sys 32 | import argparse 33 | import logging 34 | import os 35 | import collections 36 | import fnmatch 37 | import re 38 | import string 39 | import random 40 | 41 | 42 | def _read_file(file_): 43 | """Read content of file 44 | file_: either path to file or a file (like) object. 45 | Return content of file as string or 'None' if file does not exist. 46 | """ 47 | if type(file_) is str: 48 | try: 49 | file_ = open(file_, 'r') 50 | except IOError: 51 | logging.warning('file does not exist (can not read): ' + file_) 52 | return None 53 | cont_str = file_.read() 54 | file_.close() 55 | else: 56 | cont_str = file_.read() 57 | return cont_str 58 | 59 | 60 | def _read_file_lines(file_): 61 | """Read lines of file 62 | file_: either path to file or a file (like) object. 63 | Return list of lines read or 'None' if file does not exist. 64 | """ 65 | cont_str = _read_file(file_) 66 | if cont_str is None: 67 | return None 68 | return [url_str.rstrip() for url_str in cont_str.splitlines()] 69 | 70 | 71 | def _read_properties(prop_file): 72 | """Read Java property file. Comments/empty lines are ignored! 73 | Return an ordered dictionary with key/values or 'None' if file does not exist. 74 | """ 75 | lines = _read_file_lines(prop_file) 76 | if lines is None: 77 | return None 78 | splits = (tuple(l.split('=', 1)) 79 | for l in lines if len(l) > 3 and not l.strip().startswith('#')) 80 | return collections.OrderedDict((k.strip(), v.strip()) for k, v in (t for t in splits if len(t) == 2)) 81 | 82 | 83 | def _find_files(dir_, regex='*.*'): 84 | """Walk recursively through all dirs in 'dir_'. 85 | Yield all files in 'dir_' matching 'regex' with absolute path 86 | """ 87 | abs_path = os.path.abspath(dir_) 88 | if not os.path.isdir(abs_path): 89 | logging.warning('does not exist/is not a directory: ' + abs_path) 90 | for root, dirnames, filenames in os.walk(abs_path): 91 | for filename in fnmatch.filter(filenames, regex): 92 | yield os.path.join(root, filename) 93 | 94 | 95 | re = re.compile('Tr\.tr\("(.+?)"\)') 96 | def _get_tr_strings(file_): 97 | return re.findall(_read_file(file_)) 98 | 99 | 100 | def _rand_str(n, chars=string.ascii_uppercase + string.digits): 101 | """Get a string of 'n' random characters choosen out of 'chars'""" 102 | return ''.join(random.choice(chars) for _ in range(n)) 103 | 104 | 105 | def _write_file_OVERWRITE(file_path_str, str_): 106 | """Write string to file.""" 107 | dir_name, fname = os.path.split(file_path_str) 108 | if not os.path.isfile(file_path_str): 109 | logging.warning( 110 | 'file ' + file_path_str + ' does not exist, not overwriting') 111 | return False 112 | file_ = open(file_path_str, 'w') 113 | file_.write(str_) 114 | file_.close() 115 | logging.info( 116 | "wrote " + str(len(str_)) + " bytes to file: " + file_path_str) 117 | return True 118 | 119 | 120 | def _arguments(): 121 | parser = argparse.ArgumentParser() 122 | parser.add_argument("-i", "--init", action="store_true", default=False, 123 | help='initialize (ignore if properties file does not exist)') 124 | parser.add_argument( 125 | "strings_file", type=str, help="string properties file to update") 126 | parser.add_argument( 127 | "source_dir", type=str, help="base directory to seach in for Java source files") 128 | return parser.parse_args() 129 | 130 | 131 | def main(argv=sys.argv): 132 | logging.getLogger().setLevel(logging.INFO) 133 | 134 | # read strings file 135 | args = _arguments() 136 | strings_dict = _read_properties(args.strings_file) 137 | if strings_dict is None: 138 | if not args.init: 139 | logging.warning('no property file, abort') 140 | return 1 141 | strings_dict = {} 142 | 143 | # find all translation strings in Java code 144 | tr_string_list = [] 145 | for j_file in _find_files(args.source_dir, '*.java'): 146 | strings = _get_tr_strings(j_file) 147 | tr_string_list += strings 148 | #print('file: '+j_file) 149 | # print('>>>>'+'\n>>>>'.join(strings)) 150 | 151 | if not tr_string_list: 152 | logging.warning('no source strings found, abort') 153 | return 2 154 | 155 | # first, take care of strings that did not change to preserve order 156 | # and ignore unused strings 157 | upd_dict = collections.OrderedDict() 158 | for prop_key, str_ in strings_dict.items(): 159 | if str_ in tr_string_list: 160 | if str_ in upd_dict.values(): 161 | logging.warning('duplicate string in property file: "' + str_ + '"') 162 | upd_dict[prop_key] = str_ 163 | else: 164 | logging.info('removing unused string: "' + str_ + '"') 165 | 166 | # add all new strings 167 | for str_ in (s for s in tr_string_list if s not in upd_dict.values()): 168 | logging.info('adding new string: "' + str_ + '"') 169 | upd_dict['s_' + _rand_str(4)] = str_ 170 | 171 | write_str = '\n' + \ 172 | '\n'.join(v + " = " + k for v, k in upd_dict.items()) + '\n' 173 | if strings_dict == upd_dict: 174 | logging.info('no changes detected') 175 | else: 176 | if not _write_file_OVERWRITE(args.strings_file, write_str): 177 | logging.warning('could not write output, abort') 178 | return 3 179 | 180 | logging.info("Update successful") 181 | 182 | if __name__ == "__main__": 183 | sys.exit(main()) 184 | -------------------------------------------------------------------------------- /win_installer/kontalk.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kontalk/desktopclient-java/9a948bbf128f3fd30a9d3e26dbfe58b48f36500b/win_installer/kontalk.ico --------------------------------------------------------------------------------