Commit 1c376ef1 by Michał Herda

string stuff together, make UI decently working

parent 7305041d
......@@ -10,10 +10,6 @@ Check that the configuration file exists and is in the proper syntax."
(defun error-message-credentials () nil
"Account credentials are missing or improperly set in config.")
(defun error-message-furcadia-path () nil
"Furcadia path is missing or improperly set in config. Check that you have ~
used double slashes in all places while writing it.")
(defun error-login-all-accounts (condition) nil
(format nil "There was an error of type ~A while logging your accounts in. ~
Make sure that that the network connection is not interrupted."
......@@ -49,12 +45,12 @@ Check that the configuration file is writable."
(cond ((or (null accounts)
(some (lambda (x) (< (length x) 2)) accounts)
(some (compose #'not #'stringp)
(apply #'append accounts))
(some (curry #'string= "")
(apply #'append accounts)))
(note :error (error-message-credentials)))
((not (stringp (getf *config* :furcadia-path)))
(note :error (error-message-furcadia-path)))
(t
(note :info "Config file seems sane - beginning initialization.")
(note :info "Credentials seem sane - ready for initialization.")
t))))
(defun algorithm-login-all-accounts ()
......
;;;; backend-charon.lisp
(in-package :furcadia-launcher)
(defvar *charon-secret*
(error "Fill the *charon-secret* in before you compile this file.")
"The Charon API secret.")
(defvar *http-charon-clogin* "https://charon.furcadia.com/accounts/clogin.php"
"The Charon account login endpoint.")
(defun http-charon-load-account (email password)
"Performs the HTTP request to Charon account login with the provided email
and password."
(let ((version (cat "Raptor Launcher " *version*)))
(http-request *http-charon-clogin*
:method :post
:parameters `(("k" . ,*charon-secret*)
("v" . ,version)
("u" . ,email)
("p" . ,password)))))
(defun charon-load-account (email password)
"Logs the account in using the provided email and password, returning
a parsed XML response containing account and character data."
(let ((response (http-charon-load-account email password)))
(assert (vectorp response))
(let ((parsed-response
(cxml:parse-octets response (cxml-xmls:make-xmls-builder))))
(keywordize-cars parsed-response))))
(defmacro charon-load-accountf (email password)
"Logs the account in as with CHARON-LOAD-ACCOUNT, then calls SETF
STATE-CHARON-INFO on target email to set the retrieved data."
`(setf (state-charon-info *state* *state-lock* ,email)
(charon-preparse (charon-load-account ,email ,password))))
(defun charon-preparse (list)
"Preparses the whole Charon response."
(let ((character-list (cddr (caddr list))))
(loop for list in character-list
for i from 0
do (setf (nth i character-list) (charon-preparse-character list)))
character-list))
(defun charon-preparse-character (list)
"Preparses a single :CHAR entry from a Charon response."
(let* ((list (cdr list))
(desc-list (cadr list)))
(nconc (first list) (list (second list)))
(setf (cdr list) (cddr list)
(cdr desc-list) (cddr desc-list))
list))
......@@ -40,7 +40,7 @@ request."
(let* ((stream (make-string-input-stream load-character-page))
(json (decode-json stream)))
(assert (assoc :name json))
(note :info "Loaded and parsed character ~A." (cdr (assoc :snam json)))
(note :info "Loaded character ~A." (cdr (assoc :snam json)))
json))
(defun list-characters (account-json)
......@@ -82,7 +82,13 @@ format \"YYYY-MM-DD HH:MM:SS\"."
(defvar *save-char-keywords*
'("colr" "desc" "digo" "wing" "port" "tag" "adesc" "awhsp" "aresp"
"doresp" "adigo" "awing" "atime" "amaxtime" "acolr" "aport" "uid")
"Keywords to take into account when generating the save-chracter JSON.")
"Keywords to take into account when generating the save-chracter JSON
for the purpose of saving the character.")
(defvar *save-char-basic-keywords*
'("desc" "colr" "port" "uid")
"Keywords to take into account when generating the save-character JSON
purely for the purpose of fetching the download link.")
(defun construct-save-keyword (character-json keyword)
"Constructs a single key-value pair of the JSON to be saved for the provided
......@@ -91,11 +97,13 @@ character JSON and keyword."
"")))
(cons keyword (princ-to-string data))))
(defun construct-save-request (character-json fured-secret)
(defun construct-save-request (character-json fured-secret &optional basic-only)
"Constructs a full character-save JSON for the provided character JSON and
FurEd secret."
(nconc (mapcar (lambda (x) (construct-save-keyword character-json x))
*save-char-keywords*)
(if basic-only
*save-char-basic-keywords*
*save-char-keywords*))
(list (cons "tokenRequest" "true")
(cons "tokenCostume" "-1")
(cons fured-secret "1"))))
......@@ -104,10 +112,12 @@ FurEd secret."
"https://cms.furcadia.com/fured/saveCharacter.php"
"Address of the saveCharacter FurEd page.")
(defun http-save-character (character-json cookie-jar fured-secret)
(defun http-save-character (character-json cookie-jar fured-secret
&optional basic-only)
"Saves the provided character JSON, using the provided cookie jar and
FurEd secret. Returns the client login JSON."
(let* ((headers (construct-save-request character-json fured-secret))
(let* ((headers (construct-save-request character-json fured-secret
basic-only))
(result (http-request *http-save-character*
:method :post
:parameters headers
......
......@@ -2,6 +2,7 @@
(in-package :furcadia-launcher)
;; TODO make this race-condition-free, implement a lock and custom accessor
(defvar *config* nil)
(defvar *config-path* "~/.furcadia-launcher/config.lisp")
......
......@@ -8,6 +8,8 @@
(defvar *state-lock* (make-lock "Furcadia Launcher state")
"The lock for the current running state of the program.")
;;;; STATE-COOKIE
(defun state-cookie (state state-lock email)
"Returns the cookie jar for a given email in the provided state."
(with-lock-held (state-lock)
......@@ -30,6 +32,8 @@
(with-lock-held (state-lock)
(setf (gethash :cookie-jars state) new-value)))
;;;; STATE-ACCOUNT
(defun state-account (state state-lock email)
"Returns the account for a given email in the provided state."
(with-lock-held (state-lock)
......@@ -55,6 +59,35 @@
(with-lock-held (state-lock)
(setf (gethash :accounts state) new-value)))
;;;; STATE-CHARON-INFO
(defun state-charon-info (state state-lock email)
"Returns the Charon information for a given email in the provided state."
(with-lock-held (state-lock)
(let ((cookie-jars (gethash :charon-infos state)))
(assoc-value cookie-jars email :test #'string=))))
(defun (setf state-charon-info) (new-value state state-lock email)
"Sets the Charon information for a given email in the provided state."
(with-lock-held (state-lock)
(symbol-macrolet ((cookie-jars (gethash :charon-infos state)))
(setf (assoc-value cookie-jars email :test #'string=) new-value))))
(defun state-charon-infos (state state-lock)
"Returns an alist of all emails and Charon informations in the provided
state."
(with-lock-held (state-lock)
(gethash :charon-infos state)))
(defun (setf state-charon-infos) (new-value state state-lock)
"Sets the alist of all emails and Charon informations in the provided state."
(with-lock-held (state-lock)
(setf (gethash :charon-infos state) new-value)))
;;;; STATE-LAST-LOGINS
(defun state-last-logins (&optional (state *state*)
(state-lock *state-lock*))
"Returns the alist of names, shortnames and login dates of the provided
......
......@@ -15,6 +15,7 @@
:qtools-evaluator
:qtcore
:fare-csv
:cxml
:split-sequence
:esrap
:parser.common-rules
......
......@@ -58,7 +58,7 @@
(q+:show editor-box))
(define-launcher-button (button-debug "Debug")
(launcher-hide-all)
(launcher-hide-all)
(q+:show debug-box))
(define-launcher-button (button-help "Help")
......@@ -70,21 +70,19 @@
;;;; BUTTONS BELOW
(define-launcher-button (button-play "Play!")
(let* ((sname (selected-character-sname character-list))
(keep-running-p (getf furcadia-launcher::*config* :keep-running)))
(when-let ((sname (selected-character-sname character-list)))
(setf (q+:enabled button-play) nil)
(bt:make-thread (lambda ()
(furcadia-launcher::furcadia sname)
(when keep-running-p
(signal! launcher (launch-done)))))
(unless keep-running-p
(q+:close launcher))))
(signal! launcher (launch-done))))))
(define-signal (launcher launch-done) ())
(define-slot (launcher enable-play-button) ()
(define-slot (launcher post-button-play-click) ()
(declare (connected launcher (launch-done)))
(setf (q+:enabled button-play) t))
(if (getf furcadia-launcher::*config* :keep-running)
(setf (q+:enabled button-play) t)
(q+:close launcher)))
(define-launcher-button (button-sync "Sync!"))
......
......@@ -43,16 +43,17 @@
(setf (q+:text item) text)))
(defun selected-character-sname (widget)
(let* ((list (q+:selected-indexes widget))
(index (find 0 list :key #'q+:column))
(row (q+:row index))
(column (q+:column index))
(item (q+:item widget row column))
(name (q+:text item))
(last-logins (furcadia-launcher::state-last-logins))
(alist (find name last-logins :key (rcurry #'assoc-value :name)
:test #'string=)))
(assoc-value alist :shortname)))
(let ((list (q+:selected-indexes widget)))
(when list
(let* ((index (find 0 list :key #'q+:column))
(row (q+:row index))
(column (q+:column index))
(item (q+:item widget row column))
(name (q+:text item))
(last-logins (furcadia-launcher::state-last-logins))
(alist (find name last-logins :key (rcurry #'assoc-value :name)
:test #'string=)))
(assoc-value alist :shortname)))))
(define-subwidget (launcher description-preview) (q+:make-qtextbrowser)
(setf (q+:minimum-height description-preview) 100
......@@ -68,12 +69,49 @@
(characters (getf furcadia-launcher::*config* :characters))
(character (cdr (find sname characters :key #'car :test #'string=)))
(description (assoc-value character :desc)))
(setf (q+:text description-preview) description)))
(setf (q+:text description-preview) (or description ""))))
(defun launcher-logo-path ()
(let ((path (asdf:system-relative-pathname :furcadia-launcher
"img/logo-small.png")))
(cond ((probe-file path)
path)
((probe-file "img/logo-small.png")
"img/logo-small.png")
(t (note :error "The launcher logo file was not found.")
""))))
(defun safe-truename (pathname)
(handler-case (truename pathname) (error () "")))
(defun hide-chars-box-text (&optional message)
(format nil "<b><big><big>Raptor Launcher</big><br />
version ~A</big></b><br />
<i>~~ we launch raptors ~~</i><br /><br />
<img src=\"~A\"></a><br />
<p>~A</p>" *version* (safe-truename (launcher-logo-path)) message))
(define-subwidget (launcher hide-chars-box) (q+:make-qlabel)
(setf (q+:alignment hide-chars-box)
(q+:qt.align-center)
(q+:word-wrap hide-chars-box) t
(q+:text hide-chars-box)
(hide-chars-box-text "")))
(define-signal (launcher hide-chars-witty-text) ())
(define-slot (launcher make-hide-chars-witty) ()
(declare (connected launcher (hide-chars-witty-text)))
(setf (q+:text hide-chars-box)
(hide-chars-box-text (witty-line))))
(define-subwidget (launcher chars-box) (q+:make-qsplitter)
(q+:add-widget chars-box character-list)
(q+:add-widget chars-box description-preview)
(q+:add-widget chars-box hide-chars-box)
(q+:hide character-list)
(q+:hide description-preview)
(setf (q+:orientation chars-box) (q+:qt.vertical)
(q+:children-collapsible chars-box) nil
(q+:stretch-factor chars-box) (values 0 2)
(q+:stretch-factor chars-box) (values 1 1)))
(q+:stretch-factor chars-box 0) 2
(q+:stretch-factor chars-box 1) 1))
......@@ -25,7 +25,6 @@
(remove-credential-input config-accounts-widget))
(defparameter *launcher-keep-text*
;;"Keep Launcher running?"
"Keep Launcher running after launching Furcadia?")
(define-subwidget (launcher config-keep-checkbox) (q+:make-qcheckbox))
......@@ -68,7 +67,10 @@
;; Accounts
(setf (getf config :accounts)
(get-all-credentials config-accounts-widget))
(furcadia-launcher::save-config-file)))
(furcadia-launcher::algorithm-save-config-file)
;; TODO fix it - full reinitialization might not be required
;; based on the accounts that have changed
(signal! launcher (initialization-required))))
(define-slot (launcher reset-config reset-config) ()
(declare (connected config-reset (clicked)))
......
......@@ -3,7 +3,6 @@
(in-package :furcadia-launcher-gui)
(in-readtable :qtools)
(defparameter *debug-intro*
"; This is the debug read-eval-print-loop of the Common Lisp image running ~
underneath the launcher. Warning: typing stuff here CAN MAKE YOUR LAUNCHER ~
......
......@@ -3,4 +3,7 @@
(in-package :furcadia-launcher-gui)
(in-readtable :qtools)
(define-subwidget (launcher editor-box) (q+:make-qwidget))
(define-subwidget (launcher editor-box) (q+:make-qlabel "<h1>TODO</h1>")
(setf (q+:alignment editor-box)
(q+:qt.align-center)
(q+:word-wrap editor-box) t))
......@@ -5,7 +5,8 @@
(define-subwidget (launcher image) (q+:make-qpushbutton "No Character Image
\(click here to add)
\(150x400)")
\(150x400)
\(not working yet)")
(setf (q+:minimum-width image) 150
(q+:maximum-width image) 150
(q+:flat image) t
......
......@@ -5,8 +5,6 @@
;;;; MAIN LAYOUT
(defvar *version* "0.2pre")
(defmacro set-launcher-layout ()
`(mapc (curry #'apply #'q+:add-widget layout)
`(;;; IMAGE
......@@ -35,20 +33,153 @@
(define-subwidget (launcher layout) (q+:make-qgridlayout)
(setf (q+:window-title launcher) (format nil "Raptor Launcher ~A" *version*)
(q+:fixed-size launcher) (values 600 420)
(q+:layout central-widget) layout)
(q+:layout central-widget) layout
(q+:focus) character-list)
(set-launcher-layout)
(launcher-hide-all)
(disable-buttons)
(q+:show chars-box)
(q+:set-focus character-list))
(q+:show chars-box))
(defun error-text (symbol)
(format
nil
"<b>Woops - I got an error.</b><br />~A"
(case symbol
(accounts "Check that you properly input all emails and passwords.")
(login "Check that your credentials are valid.<br />
\(I got at least one login failure.)")
(fetch "I have failed to connect to FurEd.<br />
Check that your network connection is fully working.")
(chars "I have failed to fetch some of the characters.<br />
Check that your network connection is fully working.")
(t "Something went wrong, and I don't know what exactly.<br />
Check the logs for more information."))))
;;;; INITIALIZE
(define-signal (launcher initialization-required) ())
(define-slot (launcher initialize) ()
(declare (connected launcher (initialization-required))
(connected button-sync (clicked)))
(signal! launcher (hide-chars-witty-text))
(setf (q+:focus) button-config
(q+:enabled button-play) nil
(q+:enabled config-reset) nil
(q+:enabled config-save) nil)
(q+:hide character-list)
(q+:hide description-preview)
(q+:show hide-chars-box)
(signal! launcher (begin-initialization)))
(defmacro with-slot-thread (&body body)
`(bt:make-thread
(lambda ()
(handler-case (progn ,@body)
(error (e) (note :error "Thread finished with an error: ~A" e))))))
;;;; VERIFY-CONFIG-FILE
(define-signal (launcher begin-initialization) ())
(define-slot (launcher verify-config-file) ()
(declare (connected launcher (begin-initialization)))
(with-slot-thread
(if (furcadia-launcher::algorithm-verify-config-file)
(signal! launcher (verify-config-file-okay))
(signal! launcher (algorithm-failure) (error-text 'accounts)))))
;;;; LOGIN-ALL-ACCOUNTS
(define-signal (launcher verify-config-file-okay) ())
(define-slot (launcher login-all-accounts) ()
(declare (connected launcher (verify-config-file-okay)))
(signal! launcher (hide-chars-witty-text))
(with-slot-thread
(if (furcadia-launcher::algorithm-login-all-accounts)
(signal! launcher (login-all-accounts-okay))
(signal! launcher (algorithm-failure) (error-text 'login)))))
;;;; FETCH-ALL-ACCOUNTS
(define-signal (launcher login-all-accounts-okay) ())
(define-slot (launcher fetch-all-accounts) ()
(declare (connected launcher (login-all-accounts-okay)))
(signal! launcher (hide-chars-witty-text))
(with-slot-thread
(if (furcadia-launcher::algorithm-fetch-all-accounts)
(signal! launcher (fetch-all-accounts-okay))
(signal! launcher (algorithm-failure) (error-text 'fetch)))))
;;;; FETCH-ALL-CHARACTERS
(define-signal (launcher fetch-all-accounts-okay) ())
(define-slot (launcher fetch-all-characters) ()
(declare (connected launcher (fetch-all-accounts-okay)))
(signal! launcher (hide-chars-witty-text))
(with-slot-thread
(if (furcadia-launcher::algorithm-fetch-all-characters)
(signal! launcher (fetch-all-characters-okay))
(signal! launcher (algorithm-failure) (error-text 'chars)))))
;;;; FINISH-INITIALIZATION
(define-signal (launcher fetch-all-characters-okay) ())
(define-slot (launcher finish-initialization) ()
(declare (connected launcher (fetch-all-characters-okay)))
(q+:show character-list)
(q+:show description-preview)
(q+:hide hide-chars-box)
(setf (q+:text hide-chars-box)
(hide-chars-box-text "All clear!")
(q+:enabled button-play) nil
(q+:enabled config-reset) t
(q+:enabled config-save) t
(q+:enabled button-play) t
(q+:sorting-enabled character-list) nil
(q+:row-count character-list) 0)
(mapc (curry #'add-character-to-list character-list)
(furcadia-launcher::state-last-logins))
(q+:clear-selection character-list)
(setf (q+:sorting-enabled character-list) t))
;;;; EMERGENCY-UNLOCK
(define-signal (launcher algorithm-failure) (string))
(define-slot (launcher emergency-unlock) ((message string))
(declare (connected launcher (algorithm-failure string)))
(setf (q+:text hide-chars-box)
(hide-chars-box-text message)
(q+:enabled button-play) nil
(q+:enabled config-reset) t
(q+:enabled config-save) t))
;;;; MAIN
(defvar *launcher* nil)
;;;; TODO move this to a separate file in GUI, loaded last
(defun main ()
(unwind-protect
(with-main-window (launcher 'launcher)
(note :info "Raptor Launcher GUI started.")
(setf *launcher* launcher)
(setf furcadia-launcher::*config*
(furcadia-launcher::load-config-file))
(reset-config launcher))
(furcadia-launcher::algorithm-load-config-file)
(bt:make-thread
(lambda ()
(handler-case
(progn
(note :info "Downloading news.")
(furcadia-launcher::get-all-newsf)
(signal! launcher (enable-news)))
(error (e)
(note :error "Error while downloading news: ~A" e))))
:name "Raptor Launcher news downloader")
(reset-config launcher)
(signal! launcher (initialization-required)))
(setf *launcher* nil)))
......@@ -35,11 +35,7 @@
widget)))
(define-subwidget (launcher news-contents) (q+:make-qvboxlayout)
(setf (q+:contents-margins news-contents) (values 0 0 0 0))
(bt:make-thread (lambda ()
(furcadia-launcher::get-all-newsf)
(signal! launcher (enable-news)))
:name "Raptor Launcher news downloader"))
(setf (q+:contents-margins news-contents) (values 0 0 0 0)))
(define-subwidget (launcher news-contents-widget) (q+:make-qwidget)
(setf (q+:layout news-contents-widget) news-contents))
......
......@@ -33,8 +33,9 @@
(defun get-all-news ()
(handler-case
(multiple-value-bind (furcadia-news furcadia-date) (http-furcadia-news)
(note :info "Furcadia news downloaded successfully.")
(multiple-value-bind (launcher-news launcher-date) (http-launcher-news)
(note :info "All news downloaded successfully.")
(note :info "Raptor Launcher news downloaded successfully.")
(let ((urls-1 (mapcar (curry #'nth 7) furcadia-news))
(urls-2 (mapcar (curry #'nth 7) launcher-news))
(news (sort (nconc furcadia-news launcher-news) #'> :key #'car))
......
......@@ -9,10 +9,15 @@
#:fare-csv
#:split-sequence
#:lparallel.queue)
(:export #:note))
(:export #:note
#:witty-line
#:*version*))
(defpackage #:furcadia-launcher-gui
(:use #:cl+qt
#:alexandria
#:furcadia-launcher)
(:export #:main))
(defvar furcadia-launcher::*version* "0.2pre"
"The version of the Raptor Launcher.")
......@@ -21,13 +21,13 @@ a-f, false otherwise."
unless (member char chars) return nil
finally (return t))))
(defun sleepcar (function list &optional (n 5) (sleep-interval 1))
(defun sleepcar (function list &optional (n 100) (sleep-interval 1))
"Collects the element of funcalling FUNCTION on successive elements of LIST,
sleeping for SLEEP-INTERVAL seconds every N elements."
(loop for elt in list
for i from 0
for i from 1
collect (funcall function elt)
when (zerop (mod (1+ i) n))
when (zerop (mod i n))
do (sleep sleep-interval)))
(defun fformat (stream format-string &rest format-args)
......@@ -62,3 +62,48 @@ is replaced with replacement."
(format nil "~4,'0D-~2,'0D-~2,'0D ~2,'0D:~2,'0D:~2,'0D"
(nth 5 time) (nth 4 time) (nth 3 time)
(nth 2 time) (nth 1 time) (nth 0 time))))
(defun keywordize (string)
"Given a string, upcases it returns a keyword with the upcased name."
(intern (string-upcase string) :keyword))
(defun keywordize-cars (list)
"Traverses and destructively modifies the provided list by replacing the CAR
of each list with its keywordized version, if said CAR is a string."
(when (consp list)
(when (stringp (car list))
(setf (car list)
(keywordize (car list))))
(mapc #'keywordize-cars list))
list)
;;;; WITTY LINES
(defparameter *witty-lines*
'("Bouncing raptors on dragons..."
"Painting all characters green..."
"Setting all passwords to qwerty123..."
"Randomly flipping characters' genders..."
"Loading up catapults..."
"Shaving cats..."
"Obfuscating configuration..."
"Brewing original British tea on your CPU..."
"Launching all alts at once..."
"Launching pies..."
"Wasting your time..."
"Ordering higher-quality raptors..."
"Rewriting launcher code..."
"Stopping the world..."
"Loading Windows 98..."
"Retrieving the mislaunched raptor..."
"Fetching registry info..."
"Deleting Furcadia..."
"Formatting the hard drive..."
"Setting up hacker backdoors..."
"Reading floppy disk..."
"Increasing security by 25%...")
"These are meant to be funny, you know.")
(defun witty-line ()
"Returns a random witty line."
(random-elt *witty-lines*))
......@@ -2,15 +2,22 @@
(in-package :furcadia-launcher)
(defun error-message-furcadia-path () nil
"Furcadia path is missing or improperly set in config. Check that you have ~
used double slashes in all places while writing it.")
(defun furcadia (sname &optional
(config *config*) (state *state*)
(state-lock *state-lock*))
"Launches Furcadia for the character with the given shortname."
(let* ((login-link (character-login-link sname config state state-lock))
(furcadia-path (getf *config* :furcadia-path))
(process (launch-furcadia furcadia-path login-link)))
(note :info "Furcadia launched for character ~A." sname)
process))
(if (stringp (getf *config* :furcadia-path))
(let* ((login-link (character-login-link sname config state state-lock t))
(furcadia-path (getf *config* :furcadia-path))
(process (launch-furcadia furcadia-path login-link)))
(note :info "Furcadia launched for character ~A." sname)
process)
(note :error (error-message-furcadia-path))))
(defun initialize ()
"A high-level function for initializing the launcher."
......
......@@ -58,18 +58,19 @@ SETF (GETF CONFIG :CHARACTERS) on the provided config."
`(setf (getf ,config :characters)
(fetch-all-characters ,state ,state-lock)))
(defun character-login-link (sname &optional (config *config*)
(state *state*) (state-lock *state-lock*))
(defun character-login-link (sname &optional (config *config*) (state *state*)
(state-lock *state-lock*) basic-only)
"Provided a shortname, a config, a state and a state lock, returns a furc://
login link for the character with the respective shortname."
(let* ((character (assoc-value (getf config :characters) sname
:test #'string=))
(accounts (with-lock-held (state-lock) (gethash :accounts state)))
(accounts (state-accounts state state-lock))
(email-shortnames (mapcan #'make-email-shortname-alist accounts))
(email (rassoc-value email-shortnames sname :test #'string=))
(account (find email accounts :test #'string=
:key (rcurry #'assoc-value :email)))
(fured-secret (assoc-value account :session))
(cookie-jar (state-cookie state state-lock email))
(result (http-save-character character cookie-jar fured-secret)))
(result (http-save-character character cookie-jar
fured-secret basic-only)))
(extract-login-link result)))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment