Compare commits

..

10 commits

Author SHA1 Message Date
Aidan
0958fa663d
multi user updates for storage module 2020-12-03 21:59:07 -08:00
Aidan
23dd2ee3b3
startup user selection 2020-12-03 21:19:26 -08:00
Aidan
170c4cb559
add storage system, also parse groups 2020-11-30 22:04:43 -08:00
Aidan
85b3346f5d
fix defects found in testing 2020-11-25 23:19:42 -08:00
Aidan
60580189ae
refactor login process 2020-11-25 22:06:20 -08:00
Aidan
3b409dd7fb
make subscribe and contact sync requests once user is chosen 2020-11-25 21:31:05 -08:00
Aidan
3ed3b2a6d3
initial framworks. linking logic is complete. 2020-11-23 22:07:04 -08:00
Aidan Hahn
a5c8be2651
use correct string concatenation method 2019-07-16 20:10:10 -07:00
Aidan Hahn
1360406d5a
significantly more sane design 2019-07-15 22:37:09 -07:00
Aidan Hahn
fbf758efe4
initial push to private repo 2019-07-14 11:25:24 -07:00
9 changed files with 434 additions and 44 deletions

View file

@ -5,20 +5,4 @@ All notable changes to this project will be documented in this file. This change
### Changed ### Changed
- Add a new arity to `make-widget-async` to provide a different widget shape. - Add a new arity to `make-widget-async` to provide a different widget shape.
## [0.1.1] - 2019-07-06 [Unreleased]: https://git.aidanis.online/aidan/whisper/compare/HEAD
### 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

View file

@ -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 ## Installation
Download from http://example.com/FIXME. Currently: use lein run
TODO: package, wrap with a nice makefile
## Usage ## Usage
Call `lein run` in a freshly cloned repository to run Whisper. Make sure Signald is already running.
FIXME: explanation FIXME: explanation
$ java -jar whisper-0.1.0-standalone.jar [args] $ java -jar whisper-0.1.0-standalone.jar [args]
@ -30,7 +35,7 @@ FIXME: listing of options this app accepts.
## License ## License
Copyright © 2019 FIXME Copyright © 2019 Aidan Hahn
This program and the accompanying materials are made available under the This program and the accompanying materials are made available under the
terms of the Eclipse Public License 2.0 which is available at terms of the Eclipse Public License 2.0 which is available at

View file

@ -4,9 +4,15 @@
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" :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/"} :url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[com.kohlschutter.junixsocket/junixsocket-demo "2.2.0"] [com.kohlschutter.junixsocket/junixsocket-common "2.3.2"]
[seesaw "1.5.0"] [com.kohlschutter.junixsocket/junixsocket-native-common "2.3.2"]
[org.clojure/data.json "0.2.6"]] [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 :main ^:skip-aot whisper.core
:target-path "target/%s" :target-path "target/%s"
:profiles {:uberjar {:aot :all}}) :profiles {:uberjar {:aot :all}})

View file

@ -1 +0,0 @@
mortimer@Vespucci.502:1562735217

View file

@ -1,15 +1,137 @@
(ns whisper.core (ns whisper.core
"Whisper Namespace" "Whisper Namespace"
(:use [whisper.ui] (:require [clojure.tools.logging :as log]
[whisper.signald])) [clojure.data.json :as json]
[clojure.core.async :as async
:refer [<!! thread]]
[clojure.java.io :as io])
;; SEESAW GUI STUFF (:import (java.io BufferedReader InputStreamReader))
(def mainWindow (frame :title "Signald Version Reader"))
(: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 (defn -main
"packs and shows main window with signald version string" "packs and shows main window with signald version string"
[& args] [& args]
(println (config! (-> mainWindow pack! show!)
:content (reduce (fn [k v] ;; paint main chat window
(apply str [k v])) (paint-main)
(get-next-update)))))
;; 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))))

View file

@ -1,17 +1,63 @@
(ns whisper.signald (ns whisper.signald
(:require [clojure.data.json :as json]) (:require [clojure.data.json :as json]
[clojure.tools.logging :as log])
(:import (:import
(org.newsclub.net.unix AFUNIXSocket AFUNIXSocketAddress) (org.newsclub.net.unix AFUNIXSocket AFUNIXSocketAddress)
(java.io File BufferedReader InputStreamReader))) (java.io File)))
(def sigdSock (AFUNIXSocket/connectTo (def callback-table (atom {}))
(AFUNIXSocketAddress. (File. "/var/run/signald/signald.sock"))))
(def sigdRead (BufferedReader. (InputStreamReader.
(. sigdSock getInputStream))))
(def sigdWrite (. sigdSock getOutputStream))
(defn get-next-update (defn get-signald-sock
"Retrieves and parses next line of incoming data from signald" [filepath]
[] (try
(:data (json/read-str (.sigdRead readLine) :key-fn keyword))) (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
View 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
View file

@ -0,0 +1,3 @@
(ns whisper.storage
"Whisper storage module
(:require [codax]))

156
src/whisper/ui.clj Normal file
View 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}))