PageRenderTime 61ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

/src/cl-async-irc.lisp

https://gitlab.com/lispbot/cl-async-irc
Lisp | 411 lines | 347 code | 54 blank | 10 comment | 3 complexity | 05dee46cbc3cadb7feaab0ec2e529d56 MD5 | raw file
  1. ;;;; cl-async-irc.lisp
  2. (in-package #:cl-async-irc)
  3. ;; Connection
  4. (defclass connection ()
  5. ((socket :initarg :socket :reader connection-socket)
  6. (line-buffer :initform nil :accessor connection-line-buffer)
  7. (message-cb :initarg :message-cb :initform nil :reader connection-message-cb)
  8. (event-cb :initarg :event-cb :initform nil :reader connection-event-cb)
  9. (handlers :initform nil :accessor connection-handlers)
  10. (handler-id :initform 0 :accessor connection-handler-id)
  11. (ping-timer :initform nil :accessor connection-ping-timer)
  12. (logging-stream :initarg :logging-stream :initform nil :reader connection-logging-stream))
  13. (:documentation "Opaque connection class. Returned by `connect'"))
  14. (defun connect (host port &key nick user real-name connect-cb message-cb event-cb tls logging-stream ping-timeout)
  15. (let* ((connection (make-instance 'connection
  16. :message-cb message-cb
  17. :event-cb event-cb
  18. :logging-stream logging-stream)))
  19. (bb:catcher
  20. (bb:alet ((socket (connect-internal host port connection tls)))
  21. (setf (slot-value connection 'socket) socket)
  22. (when nick (nick connection nick))
  23. (when user (user connection user (or real-name user)))
  24. (when connect-cb
  25. (funcall connect-cb connection)))
  26. (t (e) (signal e)))
  27. (add-handler connection :ping #'send-pong)
  28. (when ping-timeout
  29. (start-ping-timer connection ping-timeout))
  30. connection))
  31. (defun connect-internal (host port connection tlsp)
  32. (bb:with-promise (resolve reject)
  33. (funcall (if tlsp #'as-ssl:tcp-ssl-connect #'as:tcp-connect)
  34. host port
  35. (lambda (sock bytes)
  36. (declare (ignore sock))
  37. (handle-read connection bytes))
  38. :event-cb (lambda (event)
  39. (handle-event connection event))
  40. :connect-cb (lambda (sock)
  41. (resolve sock)))))
  42. (defun disconnect (connection)
  43. (as:close-socket (connection-socket connection))
  44. (cancle-ping-timeout connection))
  45. ;; Common commands
  46. (defun nick (connection nick)
  47. (command connection "NICK" nick))
  48. (defun user (connection user realname)
  49. (command connection "USER" user "*" "*" realname))
  50. ;; TODO Support multiple channels
  51. (defun join (connection channel &optional password)
  52. (apply #'command connection "JOIN" channel (ensure-list password)))
  53. ;; TODO Support multiple channels
  54. (defun part (connection channel &optional message)
  55. (apply #'command connection "PART" channel (ensure-list message)))
  56. (defun quit (connection &optional message)
  57. (apply #'command connection "QUIT" (ensure-list message)))
  58. (defun topic (connection channel &optional topic)
  59. (apply #'command connection "TOPIC" channel (ensure-list topic)))
  60. (defun pong (connection id)
  61. (command connection "PONG" id))
  62. ;; message commands
  63. (defun privmsg (connection target message)
  64. (command connection "PRIVMSG" target message))
  65. (defun action (connection target message)
  66. (privmsg connection target
  67. (format nil "~aACTION ~a~a" (code-char 1) message (code-char 1))))
  68. ;; handlers
  69. (defun add-handler (connection command closure &key timeout predicate)
  70. (let ((id (new-handler-id connection)))
  71. (push (list command id closure predicate) (connection-handlers connection))
  72. (when timeout
  73. (as:with-delay (timeout) (remove-handler connection id)))
  74. (lambda () (remove-handler connection id))))
  75. (defun remove-handler (connection handler)
  76. (if (numberp handler)
  77. (setf (connection-handlers connection)
  78. (delete-if (lambda (x) (= (second x) handler))
  79. (connection-handlers connection)))
  80. (funcall handler)))
  81. (defun new-handler-id (connection)
  82. (with-slots (handler-id) connection
  83. (prog1 handler-id
  84. (incf handler-id))))
  85. ;; implementation
  86. (defun handle-read (connection bytes)
  87. (let* ((str (concatenate 'string (or (connection-line-buffer connection) "")
  88. (babel:octets-to-string bytes :errorp nil)))
  89. (lines (split-sequence:split-sequence #\Newline str)))
  90. (setf (connection-line-buffer connection) nil)
  91. (restart-ping-timer connection)
  92. (loop for line on lines
  93. do (progn
  94. (cond
  95. ((equal line '("")) nil) ; ignore
  96. ((null (cdr line)) ; uh, no final newline
  97. (setf (connection-line-buffer connection) (car line)))
  98. (t
  99. (handle-line connection (string-right-trim '(#\Return) (car line)))))))))
  100. (defun handle-line (connection line)
  101. (let ((msg (parse-message line)))
  102. (when (connection-logging-stream connection)
  103. (format (connection-logging-stream connection)
  104. "Message: ~a~%" msg))
  105. (process-handlers connection msg)
  106. (when-let (msg-cb (connection-message-cb connection))
  107. (funcall msg-cb connection msg))))
  108. (defun process-handlers (connection msg)
  109. (loop for h in (connection-handlers connection)
  110. when (let ((res (and (equal (car h) (message-command msg))
  111. (if (fourth h)
  112. (funcall (fourth h) connection msg)
  113. t))))
  114. res)
  115. do (unless (funcall (third h) connection msg)
  116. (remove-handler connection (second h)))))
  117. (defun send-pong (connection msg)
  118. (pong connection (car (message-params msg)))
  119. t)
  120. (defun restart-ping-timer (connection)
  121. (with-slots (ping-timer) connection
  122. (when ping-timer
  123. (as:free-event (car ping-timer))
  124. (let ((time (cdr ping-timer)))
  125. (setf ping-timer
  126. (cons (as:delay (curry #'ping-timeout connection) :time time)
  127. time))))))
  128. (defun start-ping-timer (connection time)
  129. (with-slots (ping-timer) connection
  130. (setf ping-timer
  131. (cons (as:delay (curry #'ping-timeout connection) :time time)
  132. time))))
  133. (defun final-ping-timeout (connection)
  134. (funcall (connection-event-cb connection) connection 'ping-timeout))
  135. (defun ping-timeout (connection)
  136. "Send a ping to the server and if it doesn't answer, kill it."
  137. (command connection "PING" (get-universal-time))
  138. (with-slots (ping-timer) connection
  139. (setf ping-timer
  140. (cons (as:delay (curry #'final-ping-timeout connection)
  141. :time (cdr ping-timer))
  142. (cdr ping-timer)))))
  143. (defun cancle-ping-timeout (connection)
  144. (with-slots (ping-timer) connection
  145. (when ping-timer
  146. (as:free-event (car ping-timer))
  147. (setf ping-timer nil))))
  148. (defun handle-event (connection event)
  149. (when-let (event-cb (connection-event-cb connection))
  150. (funcall event-cb connection event)))
  151. (defun command (connection command &rest args)
  152. (as:write-socket-data (connection-socket connection)
  153. (format nil "~a~{ ~a~}~@[ :~a~]~a~a" command (butlast args) (car (last args))
  154. #\return #\newline)))
  155. ;; parsing
  156. (defclass message ()
  157. ((prefix :initarg :prefix
  158. :reader message-prefix)
  159. (command :initarg :command
  160. :reader message-command)
  161. (params :initarg :params
  162. :reader message-params)))
  163. (defgeneric message-user-nick (message))
  164. (defgeneric message-user-user (message))
  165. (defgeneric message-user-host (message))
  166. (defmethod message-user-nick ((self message))
  167. (first (message-prefix self)))
  168. (defmethod message-user-user ((self message))
  169. (second (message-prefix self)))
  170. (defmethod message-user-host ((self message))
  171. (third (message-prefix self)))
  172. (defmethod print-object ((self message) stream)
  173. (print-unreadable-object (self stream :type t)
  174. (with-slots (prefix command params) self
  175. (format stream "~a ~a ~a" prefix command params))))
  176. (defun parse-message (message)
  177. (destructuring-bind (prefix command &rest params)
  178. (parse-irc-msg-internal message)
  179. (make-instance 'message
  180. :prefix prefix
  181. :command command
  182. :params params)))
  183. (defun parse-irc-msg-internal (message)
  184. (if (starts-with #\: message)
  185. (destructuring-bind (prefix . command) (split-string #\Space message)
  186. (unless command
  187. (error "Malformed message from server"))
  188. (cons (parse-irc-msg-prefix (subseq prefix 1)) (parse-irc-msg-command command)))
  189. (cons nil (parse-irc-msg-command message))))
  190. (defun parse-irc-msg-prefix (prefix)
  191. (split-sequence:split-sequence-if (lambda (x) (member x '(#\@ #\!))) prefix))
  192. (defun parse-irc-msg-command (message)
  193. (destructuring-bind (cmd . params) (split-string #\Space message)
  194. (cons (convert-command cmd) (and params (parse-irc-msg-params params)))))
  195. (defun parse-irc-msg-params (message)
  196. (if (starts-with #\: message)
  197. (list (subseq message 1))
  198. (destructuring-bind (fst-param . rest) (split-string #\Space message)
  199. (cons fst-param (and rest (parse-irc-msg-params rest))))))
  200. (defun split-string (delim string)
  201. (multiple-value-bind (res index)
  202. (split-sequence:split-sequence delim string :count 1)
  203. (cons (car res)
  204. (when (< index (length string)) (subseq string index)))))
  205. (defparameter *numeric-replies*
  206. (alist-hash-table
  207. '((001 . :rpl-welcome)
  208. (002 . :rpl-yourhost)
  209. (003 . :rpl-created)
  210. (004 . :rpl-myinfo)
  211. (005 . :rpl-bounce)
  212. (200 . :rpl-tracelink)
  213. (201 . :rpl-traceconnecting)
  214. (202 . :rpl-tracehandshake)
  215. (203 . :rpl-traceunknown)
  216. (204 . :rpl-traceoperator)
  217. (205 . :rpl-traceuser)
  218. (206 . :rpl-traceserver)
  219. (207 . :rpl-traceservice)
  220. (208 . :rpl-tracenewtype)
  221. (209 . :rpl-traceclass)
  222. (210 . :rpl-tracereconnect)
  223. (261 . :rpl-tracelog)
  224. (262 . :rpl-traceend)
  225. (211 . :rpl-statslinkinfo)
  226. (212 . :rpl-statscommands)
  227. (219 . :rpl-endofstats)
  228. (242 . :rpl-statsuptime)
  229. (243 . :rpl-statsoline)
  230. (221 . :rpl-umodeis)
  231. (234 . :rpl-servlist)
  232. (235 . :rpl-servlistend)
  233. (251 . :rpl-luserclient)
  234. (252 . :rpl-luserop)
  235. (253 . :rpl-luserunknown)
  236. (254 . :rpl-luserchannels)
  237. (255 . :rpl-luserme)
  238. (256 . :rpl-adminme)
  239. (257 . :rpl-adminloc1)
  240. (258 . :rpl-adminloc2)
  241. (259 . :rpl-adminemail)
  242. (263 . :rpl-tryagain)
  243. (302 . :rpl-userhost)
  244. (303 . :rpl-ison)
  245. (301 . :rpl-away)
  246. (305 . :rpl-unaway)
  247. (306 . :rpl-nowaway)
  248. (311 . :rpl-whoisuser)
  249. (312 . :rpl-whoisserver)
  250. (313 . :rpl-whoisoperator)
  251. (317 . :rpl-whoisidle)
  252. (318 . :rpl-endofwhois)
  253. (319 . :rpl-whoischannel)
  254. (314 . :rpl-whowasuser)
  255. (314 . :rpl-endofwhowas)
  256. (321 . :rpl-liststart)
  257. (322 . :rpl-list)
  258. (323 . :rpl-listend)
  259. (325 . :rpl-uniqopis)
  260. (324 . :rpl-channelmodeis)
  261. (331 . :rpl-notopic)
  262. (332 . :rpl-topic)
  263. (341 . :rpl-inviting)
  264. (342 . :rpl-summoning)
  265. (346 . :rpl-invitelist)
  266. (347 . :rpl-endofinvitelist)
  267. (348 . :rpl-exceptlist)
  268. (349 . :rpl-endofexceptlist)
  269. (351 . :rpl-version)
  270. (352 . :rpl-whoreply)
  271. (315 . :rpl-endofwho)
  272. (353 . :rpl-namreply)
  273. (366 . :rpl-endofnames)
  274. (364 . :rpl-links)
  275. (365 . :rpl-endoflinks)
  276. (367 . :rpl-banlist)
  277. (368 . :rpl-endofbanlist)
  278. (371 . :rpl-info)
  279. (374 . :rpl-endofinfo)
  280. (375 . :rpl-motdstart)
  281. (372 . :rpl-motd)
  282. (376 . :rpl-endofmotd)
  283. (381 . :rpl-youreoper)
  284. (382 . :rpl-rehashing)
  285. (383 . :rpl-youreservice)
  286. (391 . :rpl-time)
  287. (392 . :rpl-usersstart)
  288. (393 . :rpl-users)
  289. (394 . :rpl-endofusers)
  290. (395 . :rpl-nousers)
  291. ;; errors
  292. (401 . :err-nosuchnick)
  293. (402 . :err-nosuchserver)
  294. (403 . :err-nosuchchannel)
  295. (404 . :err-cannotsendtochan)
  296. (405 . :err-toomanychannels)
  297. (406 . :err-wasnosuchnick)
  298. (407 . :err-toomanyargets)
  299. (408 . :err-nosuchservice)
  300. (409 . :err-noorigin)
  301. (411 . :err-norecipient)
  302. (412 . :err-notexttosend)
  303. (413 . :err-notoplevel)
  304. (414 . :err-wildtoplevel)
  305. (415 . :err-badmask)
  306. (421 . :err-unknowncommand)
  307. (422 . :err-nomotd)
  308. (423 . :err-noadmininfo)
  309. (424 . :err-fileerror)
  310. (431 . :err-nonicknamegiven)
  311. (432 . :err-erroneusnickname)
  312. (433 . :err-nicknameinuse)
  313. (436 . :err-nickcollision)
  314. (437 . :err-unavailresource)
  315. (441 . :err-usernotinchannel)
  316. (442 . :err-notonchannel)
  317. (443 . :err-useronchannel)
  318. (444 . :err-nologin)
  319. (445 . :err-summondisabled)
  320. (446 . :err-userdisabled)
  321. (451 . :err-notregistered)
  322. (461 . :err-needmoreparams)
  323. (462 . :err-alreadyregistred)
  324. (463 . :err-nopermforhost)
  325. (464 . :err-passwdmismatch)
  326. (465 . :err-yourebannedcreep)
  327. (466 . :err-youwillbebanned)
  328. (467 . :err-keyset)
  329. (471 . :err-channelisfull)
  330. (472 . :err-unknownmode)
  331. (473 . :err-inviteonlychan)
  332. (474 . :err-bannedfromchan)
  333. (475 . :err-badchannelkey)
  334. (476 . :err-badchanmask)
  335. (477 . :err-nochanmodes)
  336. (478 . :err-banlistfull)
  337. (481 . :err-noprivileges)
  338. (482 . :err-chanoprivsneeded)
  339. (483 . :err-cantkillserver)
  340. (484 . :err-restricted)
  341. (485 . :err-uniqopprivsneeded)
  342. (491 . :err-nooperhost)
  343. (501 . :err-umodeunknownflag)
  344. (502 . :err-usersdontmatch))))
  345. (defparameter *commands*
  346. (alist-hash-table
  347. '(("PRIVMSG" . :privmsg)
  348. ("JOIN" . :join)
  349. ("PART" . :part)
  350. ("PING" . :ping)
  351. ("TOPIC" . :topic))
  352. :test 'equal))
  353. (defun convert-command (cmd)
  354. (if (every (lambda (x) (digit-char-p x)) cmd)
  355. (let ((num-reply (parse-integer cmd)))
  356. (or (gethash num-reply *numeric-replies*) num-reply))
  357. (or (gethash cmd *commands*) cmd)))