Cannot post slack webhook url in common lisp - common-lisp

I wanted to post slack webhook, but I get an error.
This works:
(defun post-slack ()
(drakma:http-request "https://hooks.slack.com/services/xxx"
:method :post
:content-type "application/json"
:parameters '(("payload" . "{\"username\":\"bot\",\"icon_emoji\":\":hatching_chick:\",\"text\":\"name: ~A \n email: ~A \n content: ~A \",\"as_user\":true}"))))
But this does not:
(defun post-slack (name email text)
(setq *payload* (format nil "{\"username\":\"bot\",\"icon_emoji\":\":hatching_chick:\",\"text\":\"<#U7RM4J8MR> name: ~A \n email: ~A \n text: ~A \",\"as_user\":true}" name email text))
(drakma:http-request "https://hooks.slack.com/services/xxx"
:method :post
:content-type "application/json"
:parameters '(("payload" . *payload*))))
This is my error:
The value
CAVEMAN-STUDY.CONTROLLER::*PAYLOAD*
is not of type
LIST
when binding SB-C::FAST
[Condition of type TYPE-ERROR]
Restarts:
0: [RETRY] Retry SLIME REPL evaluation request.
1: [*ABORT] Return to SLIME's top level.
2: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {1008826443}>)
Backtrace:
0: (FIND-IF-NOT #<FUNCTION (LAMBDA (DRAKMA::THING) :IN DRAKMA:HTTP-REQUEST) {22D088AB}> ("payload" . CAVEMAN-STUDY.CONTROLLER::*PAYLOAD*) :KEY #<FUNCTION CDR>)
1: (DRAKMA:HTTP-REQUEST #<PURI:URI https://hooks.slack.com/services/xxxx> :METHOD :POST :CONTENT-TYPE "application/json" :PARAMETERS ("payload" . CAVEMAN-STUDY.CON..
2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (CAVEMAN-STUDY.CONTROLLER:POST-SLACK "aaa" "bbb" "ccc") #<NULL-LEXENV>)
3: (EVAL (CAVEMAN-STUDY.CONTROLLER:POST-SLACK "aaa" "bbb" "ccase"))
--more--
Please tell me how to fix it. Thanks!

You are passing (("payload" . *payload*)) to drakma:http-request,
and *payload* here is not evaluated, i.e., it is passed as a symbol,
not its value.
What you probably want is something like
(defun post-slack (name email text)
(let ((payload (format nil "..." ...)))
(drakma:http-request "https://hooks.slack.com/services/xxx"
:method :post
:content-type "application/json"
:parameters `(("payload" . ,payload)))))
Note that I replaced setq with let.
PS. You can also use (list (cons "payload" *payload*)) instead of `(("payload" . ,payload)) if you want to avoid backquote.

Since you're talking about Slack:
https://github.com/dptd/cl-slack (seems the most up to date and documented)
https://github.com/m0cchi/cl-slack
https://github.com/kkazuo/slack-client (in Quicklisp)
https://github.com/fiddlerwoaroof/slacker (most recent, no doc)
https://github.com/stryku/jasa (2017)
There are also more Slack bots.
I discovered these with quicksearch (I didn't know it was this handy).

Related

'Required argument is not a symbol' error in let binding

In the following code, I get a Required argument is not a symbol error.
(defconstant +localhost+ (vector 127 0 0 1))
(defun ip-from-hostname (hostname)
(sb-bsd-sockets:host-ent-addresses
(sb-bsd-sockets:get-host-by-name hostname)))
(defun test-connect
(let ((ip (car (ip-from-hostname "www.google.com")))
(socket (make-instance 'sb-bsd-sockets:inet-socket :type :stream :protocol :tcp)))
(sb-bsd-sockets:socket-bind socket +localhost+ 8080)
(sb-bsd-sockets:socket-connect socket ip)
(sb-bsd-sockets:socket-send socket "GET / HTTP/1.1" nil)
(write-line (sb-bsd-sockets:socket-receive socket nil 2048))))
(test-connect)
More complete error message:
Required argument is not a symbol: ((IP
(CAR
(IP-FROM-HOSTNAME "www.google.com")))
(SOCKET
(MAKE-INSTANCE
'SB-BSD-SOCKETS:INET-SOCKET :TYPE
:STREAM :PROTOCOL :TCP)))
I've narrowed down the issue to the section calling ip-from-hostname, but the strange thing is a boiled down version of the let binding works in the REPL:
(let ((ip (sb-bsd-sockets:host-ent-addresses (sb-bsd-sockets:get-host-by-name "www.google.com"))))
(write-line (write-to-string (car ip))))
I also tried replacing the ip-from-hostname call with its body thinking that it might be something to do with the arguments, but still no luck. Any thoughts?
(defun test-connect ...
... should start with a lambda list (the list of parameters), which is missing.
Remember, the syntax for DEFUN is:
defun function-name lambda-list
[[declaration* | documentation]]
form*

Error connecting to Slack IRC gateway

I'm using the cl-irc library to connect to Slack, via the IRC gateway Slack provides.
However I'm getting the following error when I try to start the message loop with read-message-loop:
error while parsing arguments to DESTRUCTURING-BIND:
invalid number of elements in
("duncan_bayne" "Welcome" "to" "Slack" "IRC" "Gateway"
"server" "duncan_bayne!~duncan_bayne#1.2.3.4")
to satisfy lambda list
(CL-IRC:NICKNAME CL-IRC::WELCOME-MESSAGE):
exactly 2 expected, but 8 found
[Condition of type SB-KERNEL::ARG-COUNT-ERROR]
...
Backtrace:
0: ((:METHOD CL-IRC::DEFAULT-HOOK (CL-IRC:IRC-RPL_WELCOME-MESSAGE)) #<CL-IRC:IRC-RPL_WELCOME-MESSAGE irc.tinyspeck.com RPL_WELCOME {1007FC6293}>) [fast-method]
1: ((:METHOD CL-IRC::APPLY-TO-HOOKS (T)) #<CL-IRC:IRC-RPL_WELCOME-MESSAGE irc.tinyspeck.com RPL_WELCOME {1007FC6293}>) [fast-method]
2: ((:METHOD CL-IRC:IRC-MESSAGE-EVENT (T CL-IRC:IRC-MESSAGE)) #<unavailable argument> #<CL-IRC:IRC-RPL_WELCOME-MESSAGE irc.tinyspeck.com RPL_WELCOME {1007FC6293}>) [fast-method]
3: ((:METHOD CL-IRC:READ-MESSAGE (CL-IRC:CONNECTION)) #<CL-IRC:CONNECTION myob.irc.slack.com {10068E8ED3}>) [fast-method]
4: ((:METHOD CL-IRC:READ-MESSAGE-LOOP (T)) #<CL-IRC:CONNECTION myob.irc.slack.com {10068E8ED3}>) [fast-method]
5: (SB-INT:SIMPLE-EVAL-IN-LEXENV (CL-IRC:READ-MESSAGE-LOOP *CONN*) #<NULL-LEXENV>)
6: (EVAL (CL-IRC:READ-MESSAGE-LOOP *CONN*))
While in the REPL I see:
UNHANDLED-EVENT:3672562852: RPL_MYINFO: irc.tinyspeck.com duncan_bayne "IRC-SLACK gateway"
I'm not sure what I'm doing wrong here; I'm fairly sure it's not my hooks, because the problem persists even if I disable them all.
Also, I can use the connection as expected - say, joining a channel and sending messages - provided I don't try to start the message loop.
At a guess, I'd say Slack is responding to connection with an unexpected message?
The fix as suggested by #jkilski is to modify cl-irc to accept the slightly unusual (but probably standards-compilant?) responses from Slack:
(in-package #:cl-irc)
(defmethod default-hook ((message irc-rpl_welcome-message))
(with-slots
(connection host user arguments)
message
(destructuring-bind
(nickname &rest welcome-message)
arguments
(setf (user connection)
(make-user connection
:nickname nickname
:hostname host
:username user)))))
(in-package #:irc)
(defmethod default-hook ((message irc-rpl_namreply-message))
(let* ((connection (connection message)))
(destructuring-bind
(nick chan-visibility channel &optional names)
(arguments message)
(declare (ignore nick))
(let ((channel (find-channel connection channel)))
(setf (visibility channel)
(or (second (assoc chan-visibility
'(("=" :public) ("*" :private) ("#" :secret))
:test #'string=))
:unknown))
(unless (has-mode-p channel 'namreply-in-progress)
(add-mode channel 'namreply-in-progress
(make-instance 'list-value-mode :value-type :user)))
(dolist (nickname (tokenize-string names))
(let ((user (find-or-make-user connection
(canonicalize-nickname connection
nickname))))
(unless (equal user (user connection))
(add-user connection user)
(add-user channel user))
(set-mode channel 'namreply-in-progress user)
(let* ((mode-char (getf (nick-prefixes connection)
(elt nickname 0)))
(mode-name (when mode-char
(mode-name-from-char connection
channel mode-char))))
(when mode-name
(if (has-mode-p channel mode-name)
(set-mode channel mode-name user)
(set-mode-value (add-mode channel mode-name
(make-mode connection
channel mode-name))
user))))))))))
I've applied to join the dev mailing list and will be submitting a patch shortly.

CLISP open-http example

I am trying to read a series of web pages with CLISP, if they exist, but I don't understand how open-http works to skip non existing web pages.
I have the following:
(dolist (word '(a b c))
(with-open-stream (stream (ext:open-http
(format nil
"https://en.wikipedia.org/wiki/~a.html"
word)
:if-does-not-exist nil))
(when stream
(print word))))
I want to simply skip a web-page if it doesn't exist, but CLISP seems to hang and returns an "Invalid argument" error.
Could anyone explain how the argument :if-does-not-exist works and/or provide examples of how to use open-http. Thanks!
It does work for me:
(with-open-stream (stream (ext:open-http
"http://stackoverflow.com/questions/234242424242"
:if-does-not-exist nil))
(format t "~&Stream: ~A~%" stream))
Output:
;; connecting to "http://stackoverflow.com/questions/234242424242"...connected...HTTP/1.1 404 Not Found
;; HTML source of Page not found
Stream: NIL
NIL
There is a delay to get the connection, but it works.
If the page does exist:
[7]> (with-open-stream (stream (ext:open-http
"http://stackoverflow.com/questions/36003343/clisp-open-http-example"
:if-does-not-exist nil))
(format t "~&Stream: ~A~%" stream))
;; connecting to "http://stackoverflow.com/questions/36003343/clisp-open-http-example"...connected...HTTP/1.1 200 OK
Stream: #<IO INPUT-BUFFERED SOCKET-STREAM CHARACTER stackoverflow.com:80>
NIL
With Wikipedia I couldn't make it work since Wikipedia.org re-directs it to HTTPS and EXT:OPEN-HTTP neither can handle HTTPS directly, nor it can handle redirects:
Here if HTTPS is used directly:
[10]> (with-open-stream (stream (ext:open-http
"https://en.wikipedia.org/wiki/Common_Lisp"
:if-does-not-exist nil))
(format t "~&Stream: ~A~%" stream))
*** - OPEN-HTTP: "https://en.wikipedia.org/wiki/Common_Lisp" is not an HTTP URL
The following restarts are available:
ABORT :R1 Abort main loop
Break 1 [11]> :r1
If "https" is replaced by "http", CLISP doesn't construct a proper address:
[12]> (with-open-stream (stream (ext:open-http
"http://en.wikipedia.org/wiki/Common_Lisp"
:if-does-not-exist nil))
(format t "~&Stream: ~A~%" stream))
;; connecting to "http://en.wikipedia.org/wiki/Common_Lisp"...connected...HTTP/1.1 301 TLS Redirect --> "https://en.wikipedia.org/wiki/Common_Lisp"
;; connecting to "http://en.wikipedia.orghttps://en.wikipedia.org/wiki/Common_Lisp"...
*** - PARSE-INTEGER: substring "" does not have integer syntax at position 0
The following restarts are available:
ABORT :R1 Abort main loop
Break 1 [13]>

Hunchentoot: Why can't I get the session-value?

Not really a CL nor a Web programming expert, so may be I'm missing something really obvious: I try to set a session-value in page-1 and fetch the result in page-2. Nothing is displayed in page-2, though...
(ql:quickload "cl-who")
(ql:quickload "hunchentoot")
(defpackage :sessmin
(:use :cl :cl-who :hunchentoot))
(in-package :sessmin)
(defun start-server (port)
(start (make-instance 'easy-acceptor :port port)))
(setf (html-mode) :html5)
(define-easy-handler (page1 :uri "/page1") ()
(start-session)
(setf (session-value :sv) "testvalue")
(with-html-output-to-string
(*standard-output* nil :prologue t :indent t)
(:html :lang "en"
(:head
(:meta :charset "utf-8")
(:title "page1"))
(:body
(:p "Session Page 1")
(:p "Go to next page" (:a :href "page2" "here"))))))
(define-easy-handler (page2 :uri "/page2") ()
(with-html-output-to-string
(*standard-output* nil :prologue t :indent t)
(:html :lang "en"
(:head
(:meta :charset "utf-8")
(:title "page2"))
(:body
(:p "Session Page 2")
(:p "Session Page 2, value:" (session-value :sv))))))
(start-server 8080)
EDIT: Got the "("s wrong in my first version, still does not work after correction though...
Your problem is not that the session value is not set, but in your cl-who format.
(:p "Session Page 2, value:" (session-value :sv))
will not print the session value; the return value of session-value is simply ignored.
;; try this:
(:p "Session Page 2, value:" (str (session-value :sv)))
;; or, if you need to html-escape the value,
(:p "Session Page 2, value:" (esc (session-value :sv)))
Not a Hunchentoot expert, but I think you forgot to close :head.

How to reduce code duplication using method combination but keeping possible early return

I got a set of classes which represent a message that has to be handled. But there is only a limited amount of open spots for handlers. Therefore any "dispatch" of a handler handling an message object has to check first whether there is a free spot.
If there is -> dispatch.
If there is not -> do not dispatch and return corresponding message
As this part of the code will be the same in any dispatch method I figured it would be best to use the method combination facility to enforce that, but I cannot figure out how.
In my current code base I tried to use a :before method, but apparently you cannot use return in such context:
(defclass message () ((msg :initarg :msg :reader msg)))
(defclass message-ext (message)
((univ-time :initarg :univ-time :reader univ-time)))
(defparameter *open-handler* nil)
(defgeneric handle (message)
(:documentation "handle the given message appropriately"))
(defmethod handle :before ((message message))
(when (> (length *open-handler*) 1)
(return :full)))
(defmethod handle ((message message))
(push (FORMAT nil "dispatched handler") *open-handler*))
(defmethod handle ((message-ext message-ext))
(push (FORMAT nil "dispatched ext handler") *open-handler*))
(handle (make-instance 'message :msg "allemeineentchen"))
(handle (make-instance 'message-ext
:msg "rowrowrowyourboat"
:univ-time (get-universal-time)))
(handle (make-instance 'message-ext
:msg "gentlydownthestreet"
:univ-time (get-universal-time)))
Execution of a form compiled with errors.
Form:
(RETURN-FROM NIL FULL)
Compile-time error:
return for unknown block: NIL
[Condition of type SB-INT:COMPILED-PROGRAM-ERROR]
Restarts:
0: [RETRY] Retry SLIME interactive evaluation request.
1: [*ABORT] Return to SLIME's top level.
2: [TERMINATE-THREAD] Terminate this thread (#<THREAD "worker" RUNNING {100594F743}>)
Backtrace:
0: ((SB-PCL::FAST-METHOD HANDLE :BEFORE (MESSAGE)) #<unavailable argument> #<unavailable argument> #<unavailable argument>)
1: ((SB-PCL::EMF HANDLE) #<unavailable argument> #<unavailable argument> #<MESSAGE-EXT {1005961733}>)
2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (HANDLE (MAKE-INSTANCE 'MESSAGE-EXT :MSG "gentlydownthestreet" :UNIV-TIME (GET-UNIVERSAL-TIME))) #<NULL-LEXENV>)
3: (EVAL (HANDLE (MAKE-INSTANCE 'MESSAGE-EXT :MSG "gentlydownthestreet" :UNIV-TIME (GET-UNIVERSAL-TIME))))
4: ((LAMBDA () :IN SWANK:INTERACTIVE-EVAL))
Is this approach even sane, and if yes how can I do it in a working fashion? (I did already try return-from with the same result)
I think you should be using the :around method qualifier instead:
(defmethod handle :around ((message message))
(if (cddr *open-handler*)
:full
(call-next-method)))
However, a more "lispy" approach is to use the CL Condition System, e.g., something like this:
(define-condition too-many-messages (...) (...) ...)
(defun add-message (message)
(when (cddr *open-handler*)
(signal 'too-many-messages))
(push message *open-handler*))
(defmethod handle ((message message))
(add-message (FORMAT nil "dispatched handler")))
You will have to handle the condition (using, e.g., handler-bind) in addition to checking the return values of your handle function.
PS. Calling length on a list to check that it is long enough is not a very good idea - although in your case, when the list is guaranteed to be short, this might be more of a style issue.
PPS. It is not a very good idea to use the word handle as a name of your function because CL has functions which contain it (e.g., handler-case). This will complicate the search in your code in addition to confusing people reading your code.
You can't call RETURN to return from a function like that.
You would need to use RETURN-FROM with the function name. But here it would return from the method - not the generic function.
#sds has an answer. Another one would be to signal a user defined condition and handle it somewhere else. Older code used catch and throw.
A more complex undertaking would be a user defined method combination.

Resources