ircc.scm 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. ;;
  2. ;; Copyright 2022, Jaidyn Levesque <jadedctrl@posteo.at>
  3. ;;
  4. ;; This program is free software: you can redistribute it and/or
  5. ;; modify it under the terms of the GNU General Public License as
  6. ;; published by the Free Software Foundation, either version 3 of
  7. ;; the License, or (at your option) any later version.
  8. ;;
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. ;;
  17. (module ircc
  18. *
  19. ;; (irc:connect
  20. ;; irc:loop
  21. ;; irc:read-alist
  22. ;; irc:write-cmd irc:write-line
  23. ;; irc:user-set! irc:user-get
  24. ;; irc:channels irc:channel-set! irc:channel-get
  25. ;; irc:hostmask? irc:hostmask-nick irc:hostmask-ident irc:hostmask-host irc:hostmask-userhost
  26. ;; irc:user-is-self?)
  27. (import scheme
  28. (chicken base) (chicken condition) (chicken io) (chicken module)
  29. (chicken pretty-print) (chicken string) (chicken tcp)
  30. srfi-1 srfi-19 srfi-69 srfi-130
  31. openssl)
  32. ;; —————————————————————————————————————————————————————————————————————————————
  33. ;; IRC constants
  34. ;; —————————————————————————————————————————————————————————————————————————————
  35. (define RPL_WELCOME 1) (export RPL_WELCOME)
  36. (define RPL_WHOISUSER 311) (export RPL_WHOISUSER)
  37. (define RPL_ENDOFWHO 315) (export RPL_ENDOFWHO)
  38. (define RPL_ENDOFWHOIS 318) (export RPL_ENDOFWHOIS)
  39. (define RPL_LIST 322) (export RPL_LIST)
  40. (define RPL_LISTEND 323) (export RPL_LISTEND)
  41. (define RPL_TOPIC 332) (export RPL_TOPIC)
  42. (define RPL_TOPICWHOTIME 333) (export RPL_TOPICWHOTIME)
  43. (define RPL_WHOREPLY 352) (export RPL_WHOREPLY)
  44. (define RPL_NAMREPLY 353) (export RPL_NAMREPLY)
  45. (define RPL_ENDOFNAMES 366) (export RPL_ENDOFNAMES)
  46. (define RPL_MOTD 372) (export RPL_MOTD)
  47. (define RPL_MOTDSTART 375) (export RPL_MOTDSTART)
  48. (define RPL_ENDOFMOTD 376) (export RPL_ENDOFMOTD)
  49. (define ERR_NONICKNAMEGIVEN 431) (export ERR_NONICKNAMEGIVEN)
  50. (define ERR_ERRONEUSNICKNAME 432) (export ERR_ERRONEUSNICKNAME)
  51. (define ERR_NICKNAMEINUSE 433) (export ERR_NICKNAMEINUSE)
  52. ;; —————————————————————————————————————————————————————————————————————————————
  53. ;; Misc. helpers
  54. ;; —————————————————————————————————————————————————————————————————————————————
  55. ;; By Göran Weinholt, from the Scheme Cookbook
  56. ;; https://cookbook.scheme.org/format-unix-timestamp/
  57. (define (time-unix->time-utc seconds)
  58. (add-duration (date->time-utc (make-date 0 0 0 0 1 1 1970 0))
  59. (make-time time-duration 0 seconds)))
  60. ;; By Göran Weinholt, from the Scheme Cookbook
  61. ;; https://cookbook.scheme.org/format-unix-timestamp/
  62. (define (time-unix->string seconds . maybe-format)
  63. (apply date->string (time-utc->date (time-unix->time-utc seconds))
  64. maybe-format))
  65. ;; —————————————————————————————————————————————————————————————————————————————
  66. ;; Mucking around with hostmasks, no-context string checks
  67. ;; —————————————————————————————————————————————————————————————————————————————
  68. ;; Return the nick part of a hostmask
  69. (define (irc:hostmask-nick hostmask)
  70. (car (string-split hostmask "!")))
  71. ;; The username/ident part of a hostmask
  72. (define (irc:hostmask-ident hostmask)
  73. (car (string-split (cadr (string-split hostmask "!"))
  74. "@")))
  75. ;; The host part of a hostmask
  76. (define (irc:hostmask-host hostmask)
  77. (cadr (string-split hostmask "@")))
  78. ;; The user@host part of a hostmask
  79. (define (irc:hostmask-userhost hostmask)
  80. (string-append
  81. (irc:hostmask-ident hostmask) "@" (irc:hostmask-host hostmask)))
  82. ;; Return whether or not a string is likely a valid hostmask
  83. (define (irc:hostmask? string)
  84. (let ([at-! (string-contains string "!")]
  85. [at-@ (string-contains string "@")]
  86. [at-. (string-contains string ".")])
  87. (and at-! at-@ at-.
  88. (string-cursor<? at-! at-@)
  89. (string-cursor<? at-@ at-.))))
  90. ;; Remove all usermode prefixes from a user string (hostmask, nick, etc)
  91. (define (irc:trim-usermode-prefixes user-string)
  92. (string-trim user-string
  93. (lambda (char)
  94. (or (eq? char #\~)
  95. (eq? char #\&)
  96. (eq? char #\@)
  97. (eq? char #\%)
  98. (eq? char #\+)))))
  99. ;; Return whether or not the given string (username/nick/hostmask/etc) is
  100. ;; equivalent to current user.
  101. (define (irc:user-is-self? conn user-string)
  102. (string=? (irc:hostmask-nick user-string)
  103. (hash-table-ref conn 'nick)))
  104. ;; Return whether or not a string is likely a channel
  105. (define (irc:channel? string)
  106. (let ([first-char (if (string-null? string) "" (string-take string 1))])
  107. (or (string=? first-char "#")
  108. (string=? first-char "&"))))
  109. ;; ——————————————————————————————————————————————————————————————————————————————
  110. ;; Processing/saving metadata
  111. ;; ——————————————————————————————————————————————————————————————————————————————
  112. ;; The user should have more-or-less total control over how to respond to
  113. ;; received messages, but ircc has to sneakily process some responses itself,
  114. ;; to ensure basic functionality (i.e., pings, chanlist, userlist, etc.)
  115. (define (irc:process-alist-internally conn alist)
  116. (let ([command (alist-ref 'command alist)]
  117. [reply (alist-ref 'reply alist)]
  118. [sender (alist-ref 'sender alist)]
  119. [params (alist-ref 'params alist)])
  120. (if command
  121. (irc:process-command-internally conn command params sender)
  122. (irc:process-reply-internally conn reply params sender)))
  123. alist)
  124. ;; Handle some replies necssary for basic functionality
  125. (define (irc:process-reply-internally conn reply params #!optional sender)
  126. (cond [(eq? reply RPL_WELCOME)
  127. (hash-table-set! conn 'registered #t)
  128. (hash-table-set! conn 'nick (car params))]
  129. [(eq? reply RPL_TOPIC)
  130. (let ([channel (second params)]
  131. [topic (last params)])
  132. (irc:channel-set! conn channel 'topic topic))]
  133. [(eq? reply RPL_TOPICWHOTIME)
  134. (let ([channel (second params)]
  135. [setter-nick (third params)]
  136. [time (if (string? (last params))
  137. (time-unix->time-utc
  138. (string->number (last params))))])
  139. (if (time? time)
  140. (irc:channel-set! conn channel 'topic-set (time->date time))))]
  141. [(eq? reply RPL_NAMREPLY)
  142. (let ([channel (third params)]
  143. [chan-symbol (second params)]
  144. [users (map irc:trim-usermode-prefixes
  145. (string-split (cadddr params) " "))])
  146. (irc:channel-set! conn channel 'symbol chan-symbol)
  147. (map
  148. (lambda (user)
  149. (irc:channel-user-add! conn channel (irc:hostmask-nick user))
  150. (irc:user-add! conn (irc:hostmask-nick user))
  151. (if (irc:hostmask? user)
  152. (irc:user-set! conn (irc:hostmask-nick user) 'hostmask user)
  153. (irc:write-cmd conn "WHO" channel)))
  154. users))]
  155. [(eq? reply RPL_WHOREPLY)
  156. (let ([nick (sixth params)]
  157. [ident (third params)]
  158. [host (fourth params)])
  159. (irc:user-set! conn nick 'hostmask
  160. (string-append nick "!" ident "@" host)))]))
  161. ;; Handle some commands necessary for basic functionality
  162. (define (irc:process-command-internally conn command params #!optional sender)
  163. (if (and (string? sender) (irc:hostmask? sender))
  164. (irc:user-set! conn (irc:hostmask-nick sender) 'hostmask sender))
  165. (cond [(string=? command "PING")
  166. (irc:write-cmd conn "PONG" (last params))]
  167. [(and (string=? command "CAP")
  168. (string=? (second params) "ACK"))
  169. (hash-table-set! conn 'capabilities (map string->symbol (cddr params)))
  170. (irc:write-cmd conn "CAP" "END")]
  171. [(string=? command "JOIN")
  172. (let ([room-name (car params)]
  173. [new-user sender])
  174. (if (irc:user-is-self? conn new-user)
  175. (irc:channel-add! conn room-name))
  176. (irc:channel-user-add! conn room-name (irc:hostmask-nick new-user)))]
  177. [(string=? command "NICK")
  178. (irc:user-update-nick! conn sender (last params))]
  179. ;; We wanna create a private-message "channel", if it's a PM
  180. [(and (string=? command "PRIVMSG")
  181. (string? (car params))
  182. (not (irc:channel? (car params))))
  183. (let* ([user-a (if (irc:hostmask? sender)
  184. (irc:hostmask-nick sender)
  185. #f)]
  186. [user-b (car params)]
  187. [users (list user-a user-b)]
  188. [channel
  189. (if (and user-a user-b)
  190. (filter (lambda (user) (not (irc:user-is-self? conn user)))
  191. users)
  192. #f)])
  193. (if (and user-a user-b channel)
  194. (begin
  195. (irc:channel-add! conn channel)
  196. (map (lambda (user)
  197. (irc:channel-user-add! conn channel user))
  198. users))))]))
  199. ;; ——————————————————————————————————————————————————————————————————————————————
  200. ;; Metadata accessors
  201. ;; ——————————————————————————————————————————————————————————————————————————————
  202. ;; Return whether or not the given capability has been agreed upon
  203. ;; between the server and this connection
  204. (define (irc:capability? conn capability)
  205. (member capability (hash-table-ref conn 'capabilities)))
  206. ;; Add a user of the given nick to the internal list of users
  207. (define (irc:user-add! conn nick)
  208. (let ([users-table (hash-table-ref conn 'users)])
  209. (if (not (hash-table-exists? users-table nick))
  210. (hash-table-set! users-table nick '()))))
  211. ;; Remove a user from the internal list of users
  212. (define (irc:user-delete! conn nick)
  213. (hash-table-delete! (hash-table-ref conn 'users) nick))
  214. ;; Replace a user's stored alist of data with a new one
  215. (define (irc:user-set-alist! conn nick alist)
  216. (let ([users-table (hash-table-ref conn 'users)])
  217. (irc:user-add! conn nick)
  218. (hash-table-set! users-table nick alist)))
  219. ;; Return an alist of data stored relating to the given user
  220. (define (irc:user-alist conn nick)
  221. (let ([users-table (hash-table-ref conn 'users)])
  222. (irc:user-add! conn nick)
  223. (if (hash-table-exists? users-table nick)
  224. (hash-table-ref users-table nick)
  225. #f)))
  226. ;; Associate a piece of data with a user, by nick
  227. (define (irc:user-set! conn nick key value)
  228. (irc:user-set-alist!
  229. conn nick (alist-update key value (irc:user-alist conn nick))))
  230. ;; Return a piece of stored data relating to a user, by nick
  231. (define (irc:user-get conn nick key)
  232. (irc:user-add! conn nick)
  233. (alist-ref key (irc:user-alist conn nick)))
  234. ;; Add a channel of name `chan` to the internal list of channels
  235. (define (irc:channel-add! conn chan)
  236. (let ([channels-table (hash-table-ref conn 'channels)])
  237. (unless (hash-table-exists? channels-table chan)
  238. (begin
  239. (hash-table-set! (hash-table-ref conn 'channels) chan (make-hash-table))
  240. (hash-table-set! (irc:channel-table conn chan) 'users '())))))
  241. ;; Remove a channel of name `chan` from the internal list of channels
  242. (define (irc:channel-delete! conn chan)
  243. (hash-table-remove! (hash-table-ref conn 'channels) chan))
  244. ;; Return a list of saved channels by name
  245. (define (irc:channels conn)
  246. (hash-table-keys (hash-table-ref conn 'channels)))
  247. ;; Return a saved channel's table
  248. (define (irc:channel-table conn chan)
  249. (hash-table-ref (hash-table-ref conn 'channels) chan))
  250. ;; Get a stored value associated with a channel, by key
  251. (define (irc:channel-get conn chan key)
  252. (hash-table-ref (irc:channel-table conn chan) key))
  253. ;; Associate a value with a given channel, by key
  254. (define (irc:channel-set! conn chan key value)
  255. (hash-table-set! (irc:channel-table conn chan)
  256. key value))
  257. ;; Returns a list of users that are stored as members of the given channel
  258. (define (irc:channel-users conn chan)
  259. (irc:channel-get conn chan 'users))
  260. ;; Add a user to a channel's list of users, by nick
  261. (define (irc:channel-user-add! conn chan nick)
  262. (unless (member nick (irc:channel-users conn chan))
  263. (irc:channel-set!
  264. conn chan 'users
  265. (append (irc:channel-get conn chan 'users)
  266. (list nick)))))
  267. ;; Remove a user from a channel's list of users, by nick
  268. (define (irc:channel-user-delete! conn chan nick)
  269. (irc:channel-set!
  270. conn chan 'users
  271. (filter (lambda (a-nick)
  272. (not (string=? nick a-nick)))
  273. (irc:channel-users conn chan))))
  274. ;; Change a user's stored nick; in internal user-table, and channels' user lists.
  275. (define (irc:user-update-nick! conn old-hostmask new-nick)
  276. (let ([old-nick (irc:hostmask-nick old-hostmask)]
  277. [new-hostmask (string-append new-nick "!"
  278. (cadr (string-split old-hostmask "!")))])
  279. (if (irc:user-is-self? conn old-hostmask)
  280. (hash-table-set! conn 'nick new-nick))
  281. ;; Internal list of users…
  282. (irc:user-add! conn new-nick)
  283. (irc:user-set-alist!
  284. conn new-nick
  285. (alist-update 'hostmask new-hostmask
  286. (irc:user-alist conn old-nick)))
  287. (irc:user-delete! conn old-nick)
  288. ;; For all rooms…
  289. (map (lambda (chan)
  290. (irc:channel-user-delete! conn chan old-nick)
  291. (irc:channel-user-add! conn chan new-nick))
  292. (irc:channels conn))))
  293. ;; —————————————————————————————————————————————————————————————————————————————
  294. ;; Parsing lines/commands
  295. ;; —————————————————————————————————————————————————————————————————————————————
  296. ;; Construct a string to write to IRC for the given command and parameters.
  297. (define (irc:cmd->string command . parameters)
  298. (let ([parameters
  299. (append (reverse (cdr (reverse parameters)))
  300. `(,(string-append ":" (last parameters))))])
  301. (string-append
  302. command
  303. " "
  304. (reduce-right
  305. (lambda (a b)
  306. (string-append a " " b))
  307. #f
  308. parameters))))
  309. ;; Convert a string to a `msg` alist, with keys 'command', 'reply', 'params',
  310. ;; and 'sender'.
  311. (define (irc:line->alist str)
  312. (let* ([space-split (string-split str " ")]
  313. [tags (irc:line-tags str space-split)]
  314. [sender (irc:line-sender str space-split)]
  315. [verb (irc:line-verb str space-split)]
  316. [command (car verb)]
  317. [reply (and (car verb) (string->number (car verb)))]
  318. [params (irc:line-verb-params verb)])
  319. `((command . ,(if (not reply) command #f))
  320. (reply . ,reply)
  321. ,(append '(params) params)
  322. (sender . ,sender)
  323. ,(append '(tags) tags))))
  324. ;; Parses out all tags from the given line of IRC output
  325. (define (irc:line-tags str space-split)
  326. (if (not (string=? (string-take str 1) "@"))
  327. #f
  328. (let*
  329. ([first-column (car space-split)]
  330. [tag-strs (string-split (string-drop first-column 1) ";")]
  331. [tag-pairs (map
  332. (lambda (tag-str)
  333. (string-split tag-str "="))
  334. tag-strs)]
  335. [no-empty-pairs (map
  336. (lambda (tag-pair)
  337. (if (eq? (length tag-pair) 1)
  338. (append tag-pair '(""))
  339. tag-pair))
  340. tag-pairs)]
  341. [escaped-pairs
  342. (map
  343. (lambda (tag-pair)
  344. (list (car tag-pair)
  345. (string-translate* (cadr tag-pair)
  346. '(("\\s" . " ")
  347. ("\\\\" . "\\")
  348. ("\\r" . "\r")
  349. ("\\n" . "\n")))))
  350. no-empty-pairs)])
  351. escaped-pairs)))
  352. ;; Parse the sender of an IRC output line, if there is any
  353. (define (irc:line-sender str space-split)
  354. (let ([first-char (string-take str 1)])
  355. (cond
  356. [(and (string=? first-char "@")
  357. (string=? (string-take (cadr space-split) 1) ":"))
  358. (string-drop (cadr space-split) 1)]
  359. [(string=? first-char ":")
  360. (string-drop (car space-split) 1)]
  361. [#t
  362. #f])))
  363. ;; Parse out the verb (command or reply) with subsequent words into a list
  364. (define (irc:line-verb str space-split)
  365. (let ([first-char (string-take str 1)])
  366. (cond
  367. [(and (string=? first-char "@")
  368. (string=? (string-take (cadr space-split) 1) ":"))
  369. (cddr space-split)]
  370. [(or (string=? first-char "@")
  371. (string=? first-char ":"))
  372. (cdr space-split)]
  373. [#t
  374. space-split])))
  375. ;; Returns a list of parameters from the parsed-out verb section of a line
  376. (define (irc:line-verb-params verb)
  377. (let* ([params (cdr verb)]
  378. [other-params '()]
  379. [last-param '()])
  380. (map (lambda (param)
  381. (cond
  382. [(string-null? param) #f]
  383. [(and (string=? (string-take param 1) ":")
  384. (null? last-param))
  385. (set! last-param
  386. (append last-param `(,(string-drop param 1))))]
  387. [(not (null? last-param))
  388. (set! last-param (append last-param `(,param)))]
  389. [#t
  390. (set! other-params (append other-params `(,param)))]))
  391. params)
  392. (append
  393. other-params
  394. `(,(reduce-right
  395. (lambda (a b)
  396. (string-append a " " b))
  397. #f
  398. last-param)))))
  399. ;; ——————————————————————————————————————————————————————————————————————————————
  400. ;; I/O
  401. ;; ——————————————————————————————————————————————————————————————————————————————
  402. ;; Read-in the next reply or command from the server, into a parsable alist with
  403. ;; four keys:
  404. (define (irc:read-alist conn)
  405. (irc:process-alist-internally
  406. conn
  407. (irc:line->alist (irc:read-line conn))))
  408. ;; Read a single line from the IRC server
  409. (define (irc:read-line conn)
  410. (handle-exceptions exn
  411. (if (member '(timeout) (condition->list exn))
  412. (irc:read-line conn)
  413. (abort exn))
  414. (read-line (hash-table-ref conn 'out))))
  415. ;; Send a specific command to the server.
  416. (define (irc:write-cmd conn command . parameters)
  417. (irc:write-line (apply irc:cmd->string (append `(,command) parameters))
  418. conn))
  419. ;; Write a line to the IRC server connection.
  420. (define (irc:write-line text connection)
  421. (write-line text (hash-table-ref connection 'in)))
  422. ;; ——————————————————————————————————————————————————————————————————————————————
  423. ;; Main
  424. ;; ——————————————————————————————————————————————————————————————————————————————
  425. ;; Connect to the given IRC server, returning an IRC connection object.
  426. (define (irc:connect host port username nick #!optional (password #f) (realname #f))
  427. (let ([conn (make-hash-table)])
  428. (define-values (out in)
  429. (ssl-connect* hostname: host port: port))
  430. (hash-table-set! conn 'in in)
  431. (hash-table-set! conn 'out out)
  432. (hash-table-set! conn 'nick nick)
  433. (hash-table-set! conn 'realname realname)
  434. (hash-table-set! conn 'channels (make-hash-table))
  435. (hash-table-set! conn 'users (make-hash-table))
  436. (hash-table-set! conn 'capabilities '())
  437. (irc:write-cmd conn "CAP" "REQ" "userhost-in-names")
  438. (if password
  439. (irc:write-cmd conn "PASS" password))
  440. (irc:write-cmd conn "USER" username "*" "0"
  441. (if realname realname "Jane Row"))
  442. (irc:write-cmd conn "NICK" nick)
  443. conn))
  444. ;; Basic loop for using an IRC connection, using two hook functions:
  445. ;; (on-command connection command params sender tags)
  446. ;; (on-reply connection reply-code params sender tags)
  447. (define (irc:loop connection on-command on-reply #!optional (debug #f))
  448. (let* ([output (irc:read-alist connection)]
  449. [command (alist-ref 'command output)]
  450. [reply (alist-ref 'reply output)]
  451. [params (alist-ref 'params output)]
  452. [sender (alist-ref 'sender output)]
  453. [tags (alist-ref 'tags output)])
  454. (if debug
  455. (pretty-print output))
  456. (if (and on-command command)
  457. (apply on-command (list connection command params sender tags)))
  458. (if (and on-reply reply)
  459. (apply on-reply (list connection reply params sender tags)))
  460. (irc:loop connection on-command on-reply debug)))
  461. ) ;; ircc module