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
- 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

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
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

View file

@ -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}})

View file

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

View file

@ -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))))

View file

@ -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
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}))