Compare commits
10 commits
8816ea23e4
...
0958fa663d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0958fa663d | ||
|
|
23dd2ee3b3 | ||
|
|
170c4cb559 | ||
|
|
85b3346f5d | ||
|
|
60580189ae | ||
|
|
3b409dd7fb | ||
|
|
3ed3b2a6d3 | ||
|
|
a5c8be2651 | ||
|
|
1360406d5a | ||
|
|
fbf758efe4 |
9 changed files with 434 additions and 44 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -5,20 +5,4 @@ All notable changes to this project will be documented in this file. This change
|
|||
### Changed
|
||||
- Add a new arity to `make-widget-async` to provide a different widget shape.
|
||||
|
||||
## [0.1.1] - 2019-07-06
|
||||
### Changed
|
||||
- Documentation on how to make the widgets.
|
||||
|
||||
### Removed
|
||||
- `make-widget-sync` - we're all async, all the time.
|
||||
|
||||
### Fixed
|
||||
- Fixed widget maker to keep working when daylight savings switches over.
|
||||
|
||||
## 0.1.0 - 2019-07-06
|
||||
### Added
|
||||
- Files from the new template.
|
||||
- Widget maker public API - `make-widget-sync`.
|
||||
|
||||
[Unreleased]: https://github.com/your-name/whisper/compare/0.1.1...HEAD
|
||||
[0.1.1]: https://github.com/your-name/whisper/compare/0.1.0...0.1.1
|
||||
[Unreleased]: https://git.aidanis.online/aidan/whisper/compare/HEAD
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -1,13 +1,18 @@
|
|||
# whisper
|
||||
# Whisper
|
||||
|
||||
FIXME: description
|
||||
Whisper is a client for signald. In combination, the two represent a complete replacement for the official Signal desktop client. Additionally, Whisper and Signald can be run on any mobile device supporting the Java Runtime Environment.
|
||||
|
||||
**Whisper is in early stages of development.**
|
||||
|
||||
## Installation
|
||||
|
||||
Download from http://example.com/FIXME.
|
||||
Currently: use lein run
|
||||
TODO: package, wrap with a nice makefile
|
||||
|
||||
## Usage
|
||||
|
||||
Call `lein run` in a freshly cloned repository to run Whisper. Make sure Signald is already running.
|
||||
|
||||
FIXME: explanation
|
||||
|
||||
$ java -jar whisper-0.1.0-standalone.jar [args]
|
||||
|
|
@ -30,7 +35,7 @@ FIXME: listing of options this app accepts.
|
|||
|
||||
## License
|
||||
|
||||
Copyright © 2019 FIXME
|
||||
Copyright © 2019 Aidan Hahn
|
||||
|
||||
This program and the accompanying materials are made available under the
|
||||
terms of the Eclipse Public License 2.0 which is available at
|
||||
|
|
|
|||
12
project.clj
12
project.clj
|
|
@ -4,9 +4,15 @@
|
|||
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
|
||||
:url "https://www.eclipse.org/legal/epl-2.0/"}
|
||||
:dependencies [[org.clojure/clojure "1.10.0"]
|
||||
[com.kohlschutter.junixsocket/junixsocket-demo "2.2.0"]
|
||||
[seesaw "1.5.0"]
|
||||
[org.clojure/data.json "0.2.6"]]
|
||||
[com.kohlschutter.junixsocket/junixsocket-common "2.3.2"]
|
||||
[com.kohlschutter.junixsocket/junixsocket-native-common "2.3.2"]
|
||||
[cljfx "1.7.10"]
|
||||
[org.clojure/data.json "0.2.6"]
|
||||
[org.clojure/tools.logging "1.1.0"]
|
||||
[org.clojure/core.cache "1.0.207"]
|
||||
[org.clojure/core.async "1.3.610"]
|
||||
[clj.qrgen "0.4.0"]
|
||||
[codax "1.3.1"]]
|
||||
:main ^:skip-aot whisper.core
|
||||
:target-path "target/%s"
|
||||
:profiles {:uberjar {:aot :all}})
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
mortimer@Vespucci.502:1562735217
|
||||
|
|
@ -1,15 +1,137 @@
|
|||
(ns whisper.core
|
||||
"Whisper Namespace"
|
||||
(:use [whisper.ui]
|
||||
[whisper.signald]))
|
||||
(:require [clojure.tools.logging :as log]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.core.async :as async
|
||||
:refer [<!! thread]]
|
||||
[clojure.java.io :as io])
|
||||
|
||||
;; SEESAW GUI STUFF
|
||||
(def mainWindow (frame :title "Signald Version Reader"))
|
||||
(:import (java.io BufferedReader InputStreamReader))
|
||||
|
||||
(:use [whisper.ui]
|
||||
[whisper.signald]
|
||||
[whisper.storage]))
|
||||
|
||||
;; TODO: config management
|
||||
;; TODO: make config dir
|
||||
(def dbpath "/tmp/temp_db")
|
||||
(def sockpath "/var/run/signald/signald.sock")
|
||||
(def user (atom {}))
|
||||
(def link-status (atom false))
|
||||
|
||||
;; TODO: handle multi users (with list ui)
|
||||
(defn handle-users
|
||||
[sigd data]
|
||||
(paint-user-select
|
||||
(get-in data ["data" "accounts"])
|
||||
true
|
||||
(fn [x _]
|
||||
;; TODO: dont magic string this
|
||||
(if (= (get x "username") "Link New User")
|
||||
(make-req sigd {"type" "link"})
|
||||
(do (reset! user x)
|
||||
(init-user-schema (get x "username")))))))
|
||||
|
||||
(defn digest-group-or-contact-list
|
||||
[list db]
|
||||
;; Should we iter in a background thread?
|
||||
(doseq [contact list]
|
||||
(let [id (get-id-from-contact-or-group contact)
|
||||
name (get contact "name")]
|
||||
(store-contact db (get @user "username") id contact)
|
||||
(add-json-contact {:id id :name name}))))
|
||||
|
||||
(defn -main
|
||||
"packs and shows main window with signald version string"
|
||||
[& args]
|
||||
(println (config! (-> mainWindow pack! show!)
|
||||
:content (reduce (fn [k v]
|
||||
(apply str [k v]))
|
||||
(get-next-update)))))
|
||||
|
||||
;; paint main chat window
|
||||
(paint-main)
|
||||
|
||||
;; prep to read signald
|
||||
;; also open storage
|
||||
(let [sigd (get-signald-sock sockpath)
|
||||
db (init-db dbpath)]
|
||||
|
||||
;; check if signald conn failed
|
||||
(if (nil? sigd)
|
||||
(add-str-flag "No signald connection."))
|
||||
|
||||
;; register callbacks between the UI and signald
|
||||
(assoc-callback-type "version" (fn [x] (add-str-flag (get-version-from-version-message x))))
|
||||
(assoc-callback-type "linking_uri" (fn [x] (paint-linking x)))
|
||||
(assoc-callback-type "linking_successful" (fn [_] (paint-linking nil)
|
||||
(reset! link-status true)))
|
||||
(assoc-callback-type "linking_error" (fn [_] (paint-linking nil)
|
||||
(log/error "Linking failed")
|
||||
(add-str-flag "Linking failed")
|
||||
(reset! link-status true)))
|
||||
(assoc-callback-type "unexpected_error" (fn [x] (add-str-flag (str "Unexpected error: "
|
||||
(get-in x ["data" "message"])))))
|
||||
(assoc-callback-type "contacts_list" (fn [x] (digest-group-or-contact-list
|
||||
(get-in x ["data"]) db)))
|
||||
(assoc-callback-type "group_list" (fn [x] (digest-group-or-contact-list
|
||||
(get-in x ["data" "groups"]) db)))
|
||||
|
||||
;; get io channels for signald
|
||||
(let [sigd-input (io/writer (.getOutputStream sigd))
|
||||
sigd-output (BufferedReader. (InputStreamReader. (.getInputStream sigd)))
|
||||
sigd-loop (thread (callback-loop sigd-output))]
|
||||
|
||||
;; handle login basically
|
||||
(assoc-callback-type "account_list" (fn [x] (handle-users sigd-input x)))
|
||||
|
||||
;; log in every time user state changes
|
||||
;; another way we could do this is to have a callback triggered on a
|
||||
;; signald message type of "subscribed"
|
||||
;; but I thought it would be more efficient to just watch for results
|
||||
;; from the call to handle-users
|
||||
;; ALSO: close account picker
|
||||
(add-watch user :login-manager
|
||||
(fn [key atom old-state new-state]
|
||||
(let [old-user (get old-state "username")
|
||||
new-user (get new-state "username")]
|
||||
|
||||
(if (not= old-user new-user)
|
||||
(do (log/info key " changed state from "
|
||||
old-user " to " new-user)
|
||||
|
||||
;; unsub from old acc
|
||||
(if (some? old-user)
|
||||
;; TODO: make this a, do statement
|
||||
;; add a call to reset the UI
|
||||
(make-req sigd-input ("type" "unsubscribe"
|
||||
"username" old-user)))
|
||||
|
||||
;; sub to new account
|
||||
(if (some? new-user)
|
||||
(do
|
||||
(make-req sigd-input {"type" "subscribe"
|
||||
"username" new-user})
|
||||
(make-req sigd-input {"type" "sync_contacts"
|
||||
"username" new-user})
|
||||
(make-req sigd-input {"type" "list_contacts"
|
||||
"username" new-user})
|
||||
(make-req sigd-input {"type" "list_groups"
|
||||
"username" new-user})
|
||||
;; TODO: load messages or something
|
||||
;; maybe wait till here to paint main UI
|
||||
(add-str-flag (str "Now logged in as " new-user))))
|
||||
|
||||
;; close account picker
|
||||
(paint-user-select [] false (fn [_ _] (log/error "this cannot be called"))))))))
|
||||
|
||||
;; whenever a link account operation finishes, retrigger handle-users
|
||||
(add-watch link-status :linking-manager
|
||||
(fn [key atom old-state new-state]
|
||||
(if (and (not= old-state new-state) new-state)
|
||||
(do
|
||||
(log/info key " has detected a link account operation finished.")
|
||||
(reset! link-status false)
|
||||
(make-req sigd-input {"type" "list_accounts"})))))
|
||||
|
||||
;; find avail accounts
|
||||
(make-req sigd-input {"type" "list_accounts"})
|
||||
|
||||
;; finally, block on signald loop till return
|
||||
(<!! sigd-loop))))
|
||||
|
|
|
|||
|
|
@ -1,17 +1,63 @@
|
|||
(ns whisper.signald
|
||||
(:require [clojure.data.json :as json])
|
||||
(:require [clojure.data.json :as json]
|
||||
[clojure.tools.logging :as log])
|
||||
(:import
|
||||
(org.newsclub.net.unix AFUNIXSocket AFUNIXSocketAddress)
|
||||
(java.io File BufferedReader InputStreamReader)))
|
||||
(java.io File)))
|
||||
|
||||
(def sigdSock (AFUNIXSocket/connectTo
|
||||
(AFUNIXSocketAddress. (File. "/var/run/signald/signald.sock"))))
|
||||
(def sigdRead (BufferedReader. (InputStreamReader.
|
||||
(. sigdSock getInputStream))))
|
||||
(def sigdWrite (. sigdSock getOutputStream))
|
||||
(def callback-table (atom {}))
|
||||
|
||||
(defn get-next-update
|
||||
"Retrieves and parses next line of incoming data from signald"
|
||||
[]
|
||||
(:data (json/read-str (.sigdRead readLine) :key-fn keyword)))
|
||||
(defn get-signald-sock
|
||||
[filepath]
|
||||
(try
|
||||
(let [target (AFUNIXSocketAddress. (File. filepath))]
|
||||
(log/info "Connected to signald at: " filepath)
|
||||
(AFUNIXSocket/connectTo target))
|
||||
(catch Exception e
|
||||
(log/error e "Caught exception connecting to socket")
|
||||
nil)))
|
||||
|
||||
(defn assoc-callback-type
|
||||
[callback-token callback]
|
||||
(swap! callback-table assoc callback-token callback))
|
||||
|
||||
(defn disassoc-callback-type
|
||||
[callback-token]
|
||||
(swap! callback-table dissoc callback-token))
|
||||
|
||||
;; TODO wrap attempt to call callback func in try catch
|
||||
(defn callback-loop
|
||||
[BufferedReader]
|
||||
(doall (for [line (line-seq BufferedReader)]
|
||||
(let [obj (json/read-str line)
|
||||
type-tok (get obj "type")
|
||||
callback-func (get @callback-table type-tok)]
|
||||
(log/info "Received message from signald (type: " type-tok ").")
|
||||
(if (nil? callback-func)
|
||||
(log/error "No callback for message type: " type-tok)
|
||||
(callback-func obj))))))
|
||||
|
||||
(defn get-version-from-version-message
|
||||
[version-message-obj]
|
||||
(let [data (get version-message-obj "data")]
|
||||
(str (get data "name") " "
|
||||
(get data "version") " "
|
||||
(get data "branch"))))
|
||||
|
||||
(defn get-id-from-contact-or-group
|
||||
[contact]
|
||||
(let [c-id (get-in contact ["address" "number"])
|
||||
g-id (get-in contact ["groupId"])]
|
||||
(if (nil? c-id)
|
||||
(if (nil? g-id)
|
||||
(log/error "Document had no contact id or group id")
|
||||
g-id)
|
||||
c-id)))
|
||||
|
||||
;; sends data to signal socket
|
||||
;; expects data to be a map
|
||||
;; adds a newline
|
||||
(defn make-req
|
||||
[output-stream data]
|
||||
(try (.write output-stream (str (json/write-str data) "\n")) (.flush output-stream)
|
||||
(catch Exception e (log/error e "Error sending to signald"))))
|
||||
|
|
|
|||
69
src/whisper/storage.clj
Normal file
69
src/whisper/storage.clj
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
(ns whisper.storage
|
||||
"Whisper storage module"
|
||||
(:require [clojure.tools.logging :as log]
|
||||
[codax.core :as c]))
|
||||
|
||||
(defn init-db
|
||||
"opens db at filepath and makes sure schema is in place"
|
||||
[filepath]
|
||||
(let [db (c/open-database! filepath)]
|
||||
;; TODO: schema preperations here
|
||||
db))
|
||||
|
||||
(defn init-user-schema
|
||||
[db username]
|
||||
(let [user-doc (c/get-at! db [username])
|
||||
user-contact-doc (get user-doc :contacts)
|
||||
user-messages-doc (get user-doc :messages)]
|
||||
|
||||
(if (nil? user-doc)
|
||||
(c/assoc-at! db [username] {:contacts {}
|
||||
:messages {}}))
|
||||
(if (nil? user-contact-doc)
|
||||
(c/assoc-at! db [username :contacts] {}))
|
||||
|
||||
(if (nil? user-messages-doc)
|
||||
(c/assoc-at! db [username :messages] {}))))
|
||||
|
||||
(defn store-contact
|
||||
"adds a new contact to db"
|
||||
[db username contact-id contact-doc]
|
||||
(let [curr-doc (c/get-at! db [username :contacts contact-id])]
|
||||
(if (or (nil? curr-doc) (not= curr-doc contact-doc))
|
||||
(c/assoc-at! db [username :contacts contact-id] contact-doc)))
|
||||
(if (nil? (c/get-at! db [username :messages contact-id]))
|
||||
(c/assoc-at! db [username :messages contact-id] [])))
|
||||
|
||||
(defn store-message
|
||||
"adds a message to the db"
|
||||
[db username contact-id message-doc]
|
||||
(let [curr-thread (c/get-at! db [username :messages contact-id])]
|
||||
;; no contact? dont bother
|
||||
(if (nil? curr-thread)
|
||||
(log/info "Received message for undiscovered contact (wont store).")
|
||||
(c/merge-at! db [username :messages contact-id] [message-doc]))))
|
||||
|
||||
(defn get-contacts
|
||||
"retrieve all contacts"
|
||||
[db username]
|
||||
(c/get-at! db [username :contacts]))
|
||||
|
||||
(defn get-contact
|
||||
"retrieve a specific contact"
|
||||
[db username contact-id]
|
||||
(c/get-at! db [username :contacts contact-id]))
|
||||
|
||||
(defn get-thread
|
||||
"get all messages to and from a specific contact/group"
|
||||
[db username contact-id]
|
||||
(c/get-at! db [username :messages contact-id]))
|
||||
|
||||
(defn get-thread-and-contact-if-contact
|
||||
"get all messages to and from a contact, as well as their contact info IF they are a stored contact"
|
||||
[db username contact-id]
|
||||
(let [contact-doc (c/get-at! db [username :contacts contact-id])]
|
||||
(if (not (nil? contact-doc))
|
||||
;; contact exists
|
||||
{:contact contact-doc :thread (c/get-at! db [username :messages contact-id])}
|
||||
;; else return nil
|
||||
nil)))
|
||||
3
src/whisper/storage.clj~
Normal file
3
src/whisper/storage.clj~
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
(ns whisper.storage
|
||||
"Whisper storage module
|
||||
(:require [codax]))
|
||||
156
src/whisper/ui.clj
Normal file
156
src/whisper/ui.clj
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
(ns whisper.ui
|
||||
"Whisper UI Module"
|
||||
(:require [cljfx.api :as fx]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.tools.logging :as log]
|
||||
[clojure.core.cache :as cache]
|
||||
[clj.qrgen :as qr]
|
||||
[clojure.java.io :as io])
|
||||
|
||||
(:import [net.glxn.qrgen.core.image ImageType]
|
||||
[java.io FileInputStream]
|
||||
[javafx.scene.image Image WritableImage]))
|
||||
|
||||
;; simple permutation of json into label
|
||||
(defn paint-message
|
||||
[message]
|
||||
{:fx/type :label
|
||||
:text (json/write-str message)})
|
||||
|
||||
(defn paint-contact
|
||||
[contact]
|
||||
{:fx/type :v-box
|
||||
:children [{:fx/type :label
|
||||
:scale-x 1.1
|
||||
:text (get contact "name")}
|
||||
{:fx/type :label
|
||||
:scale-x 0.9
|
||||
:text (get contact "id")}]})
|
||||
|
||||
(defn paint-flag
|
||||
[flag]
|
||||
{:fx/type :label
|
||||
:v-box/margin 10
|
||||
:text (json/write-str flag)})
|
||||
|
||||
;; atomic global state for the UI
|
||||
(def main-context (atom (fx/create-context
|
||||
{:contacts []
|
||||
:messages []
|
||||
:flags []}
|
||||
cache/lru-cache-factory)))
|
||||
|
||||
(def qrcode-context (atom (fx/create-context
|
||||
{:message "In Progress..." }
|
||||
cache/lru-cache-factory)))
|
||||
|
||||
(defn add-json-message
|
||||
[obj]
|
||||
(swap! main-context fx/swap-context update :messages conj (paint-message obj)))
|
||||
|
||||
(defn add-json-contact
|
||||
[obj]
|
||||
(swap! main-context fx/swap-context update :contacts conj (paint-contact obj)))
|
||||
|
||||
(defn add-str-flag
|
||||
[str]
|
||||
(swap! main-context fx/swap-context update :flags conj
|
||||
(paint-flag str)))
|
||||
|
||||
;; TODO: maybe an actual UI lol
|
||||
(defn main-ui
|
||||
[{:keys [fx/context]}]
|
||||
{:fx/type :stage
|
||||
:showing true
|
||||
:width 500
|
||||
:height 500
|
||||
:scene {:fx/type :scene
|
||||
:root {:fx/type :v-box
|
||||
:children (conj (fx/sub-val context :flags)
|
||||
{:fx/type :h-box
|
||||
:children [{:fx/type :v-box
|
||||
:children (fx/sub-val context :contacts)}
|
||||
;; TODO: scrollable view (list view?)
|
||||
{:fx/type :v-box
|
||||
:children (fx/sub-val context :messages)}
|
||||
;; TODO: compose message view
|
||||
]})}}})
|
||||
|
||||
(defn qrcode-ui
|
||||
[{:keys [showing image]}]
|
||||
{:fx/type :stage
|
||||
:showing showing
|
||||
:title "link device"
|
||||
:width 500
|
||||
:height 500
|
||||
:scene {:fx/type :scene
|
||||
:root {:fx/type :v-box
|
||||
:children [{:fx/type :image-view
|
||||
:fit-height 480
|
||||
:fit-width 480
|
||||
:image image}]}}})
|
||||
|
||||
(defn user-select-ui
|
||||
[{:keys [users callback showing]}]
|
||||
{:fx/type :stage
|
||||
:showing showing
|
||||
:title "select a user"
|
||||
:width 200
|
||||
:height 500
|
||||
:scene {:fx/type :scene
|
||||
:root {:fx/type :v-box
|
||||
:children [{:fx/type :label
|
||||
:v-box/margin 5
|
||||
:text "Pick a user to continue"}
|
||||
{:fx/type fx/ext-let-refs
|
||||
:refs {::toggle-group {:fx/type :toggle-group}}
|
||||
:desc {:fx/type :v-box
|
||||
:padding 20
|
||||
:spacing 10
|
||||
:children (for [user users]
|
||||
{:fx/type :radio-button
|
||||
:toggle-group {:fx/type fx/ext-get-ref
|
||||
:ref ::toggle-group}
|
||||
:selected false
|
||||
:text (get user "username")
|
||||
:on-action #(callback user %)})}}]}}})
|
||||
|
||||
(def main-renderer (fx/create-renderer
|
||||
:middleware (comp
|
||||
fx/wrap-context-desc
|
||||
(fx/wrap-map-desc (fn [_] {:fx/type main-ui})))
|
||||
:opts {:fx.opt/type->lifecycle #(or (fx/keyword->lifecycle %)
|
||||
(fx/fn->lifecycle-with-context %))}))
|
||||
|
||||
(def aux-renderer (fx/create-renderer))
|
||||
|
||||
(defn paint-main
|
||||
[]
|
||||
(fx/mount-renderer main-context main-renderer))
|
||||
|
||||
(defn paint-linking
|
||||
[obj]
|
||||
(if (nil? obj)
|
||||
;; if no obj, turn off window
|
||||
(aux-renderer {:fx/type qrcode-ui
|
||||
:showing false
|
||||
:image (WritableImage. 1 1)
|
||||
:message "lol"})
|
||||
|
||||
;; else gen qr
|
||||
(let [uri (str (get-in obj ["data" "uri"]))
|
||||
image (io/file (qr/from uri))]
|
||||
(aux-renderer {:fx/type qrcode-ui
|
||||
:showing true
|
||||
:image (Image. (FileInputStream. image))}))))
|
||||
|
||||
;; TODO: modify callback into event handler
|
||||
(defn paint-user-select
|
||||
[users showing callback]
|
||||
(aux-renderer
|
||||
{:fx/type user-select-ui
|
||||
:showing showing
|
||||
;; "Make New User" is a magic token. see handle-users in core
|
||||
;; TODO: dont
|
||||
:users (conj users {"username" "Link New User"})
|
||||
:callback callback}))
|
||||
Loading…
Add table
Add a link
Reference in a new issue