/irc-process-line.rkt

http://github.com/elibarzilay/rudybot · Racket · 853 lines · 667 code · 75 blank · 111 comment · 147 complexity · 6cada44c46522426e829b7e448344256 MD5 · raw file

  1. #lang racket
  2. (provide irc-process-line)
  3. (require scheme/sandbox
  4. scheme/system
  5. srfi/13
  6. srfi/14
  7. (except-in "sandboxes.rkt" main)
  8. "vars.rkt"
  9. "git-version.rkt"
  10. "userinfo.rkt"
  11. "utils.rkt"
  12. (except-in "xlate.rkt" main)
  13. (except-in "spelled-out-time.rkt" main)
  14. (except-in "quotes.rkt" main)
  15. (except-in "re.rkt" main)
  16. (except-in "tinyurl.rkt" main)
  17. (planet schematics/macro/macro)
  18. (planet neil/numspell/numspell)
  19. )
  20. (define (is-master?)
  21. (let ([mm (unbox *my-master*)] [id (*full-id*)])
  22. (cond [(regexp? mm) (regexp-match? mm id)]
  23. [(string? mm) (equal? mm id)]
  24. [else #f])))
  25. ;; (colon w) is a pattern that matches coloned words, and registers
  26. ;; their position
  27. (define (starts-with-colon str)
  28. (cond [(regexp-match-positions #rx"^:(.*)" str)
  29. => (lambda (m) (substring+posn str (caadr m)))]
  30. [else #f]))
  31. (define-match-expander colon
  32. (syntax-rules ()
  33. [(colon w) (app starts-with-colon w)]))
  34. (define (describe-since when)
  35. (spelled-out-time (- (current-seconds) when)))
  36. (define (nick->sighting-string n)
  37. ;; We might have accidentally stored a bunch of sightings for this
  38. ;; nick. If we were to display them all, they might get truncated,
  39. ;; due to the 500-character output limit. So userinfo always gives
  40. ;; us at most two of the recent ones.
  41. (let ([ss (lookup-sightings n)])
  42. (if (null? ss)
  43. (format "No sign of ~a" n)
  44. (string-join
  45. (map (lambda (info)
  46. (format "~a was seen ~ain/on ~a ~a ago~a"
  47. (sighting-who info)
  48. (aif it (sighting-action? info) (string-append it " ") "")
  49. (sighting-where info)
  50. (describe-since (sighting-when info))
  51. (let ([words (string-join (sighting-words info))])
  52. (if (positive? (string-length words))
  53. (format ", saying \"~a\"" words)
  54. ""))))
  55. ss)
  56. ", and then "))))
  57. ;; For rate limiting -- every time we respond to a direct request, we
  58. ;; save the time under the requstor's nick. That way, we can later
  59. ;; check a request from the same nick to see if they've requested
  60. ;; something recently, and perhaps deny the request.
  61. (define *action-times-by-nick* (make-hash))
  62. (define (we-recently-did-something-for nick)
  63. (>= (hash-ref *action-times-by-nick* nick 0)
  64. (- (current-seconds) 10)))
  65. (define (note-we-did-something-for! for-whom)
  66. (hash-set! *action-times-by-nick* for-whom (current-seconds)))
  67. ;; Cheap global bit to avoid nagging channels with grab instructions (doesn't
  68. ;; work when give is used in several channels at the same time, no care for
  69. ;; races)
  70. (define last-give-instructions #f)
  71. (define (out format-string . args)
  72. (let* ([str (apply format format-string args)]
  73. [str (if (> (string-length str) *max-output-line*)
  74. (string-append (substring str 0 (- *max-output-line* 4)) " ...")
  75. str)]
  76. ;; don't display newlines, so that Bad Guys won't be able
  77. ;; to inject IRC commands into our output.
  78. [str (regexp-replace* #rx"[\n\r]" str " <NEWLINE> ")])
  79. (log "=> ~a" str)
  80. (fprintf (*irc-output*) "~a~%" str)))
  81. (define (pm #:notice? [notice? #f] target fmt . args)
  82. (out "~a" (format "~a ~a :~a"
  83. (if notice? "NOTICE" "PRIVMSG")
  84. target (apply format fmt args))))
  85. ;; ----------------------------------------------------------------------------
  86. ;; General IRC protocol matchers
  87. (defmatcher IRC-COMMAND "ERROR"
  88. (log "Uh oh!"))
  89. (define (send-NICK-and-USER)
  90. (when (eq? (unbox *authentication-state*) 'havent-even-tried)
  91. (out "NICK ~a" (unbox *my-nick*))
  92. ;; RFC 1459 suggests that most of this data is ignored.
  93. (out "USER luser unknown-host localhost :Eric Hanchrow's bot, version ~a"
  94. (git-version))
  95. (if (*nickserv-password*)
  96. (pm "NickServ" "identify ~a" (*nickserv-password*))
  97. (log "I'd register my nick, if I had a password."))
  98. (set-box! *authentication-state* 'tried)))
  99. ;; This message doesn't contain much information; it really just means
  100. ;; we've connected. And not all servers emit this anyway. The server
  101. ;; on freenode did up to about January 2010
  102. (defmatcher IRC-COMMAND "NOTICE"
  103. (send-NICK-and-USER))
  104. (defmatcher IRC-COMMAND "PING"
  105. (out "PONG ~a" (car (*current-words*))))
  106. (defmatcher IRC-COMMAND (regexp #rx"^:((.*)!(.*)@(.*))$"
  107. (list _ full-id nick id host))
  108. (define (espy target action words)
  109. (note-sighting (make-sighting nick target (current-seconds) action words)))
  110. (if (equal? nick (unbox *my-nick*))
  111. (match (*current-words*)
  112. [(list "NICK" (colon new-nick))
  113. (log "I seem to be called ~s now" new-nick)
  114. (set-box! *my-nick* new-nick)]
  115. [_ (log "I seem to have said ~s" (*current-words*))])
  116. (match (*current-words*)
  117. [(list "KICK" target victim mumblage ...)
  118. (espy target (format "kicking ~a" victim) mumblage)]
  119. [(list "MODE" target mode-data ...)
  120. (espy target (format "changing the mode to '~a'" mode-data) '())]
  121. [(list "INVITE" lucky-recipient (colon party) further ...)
  122. (espy host (format "inviting ~a to ~a" lucky-recipient party)
  123. further)]
  124. [(list "NICK" (colon first-word) rest ...)
  125. (espy host (format "changing their nick to ~a" first-word) '())]
  126. [(list "TOPIC" target (colon first-word) rest ...)
  127. (espy target
  128. (format "changing the channel's topic to '~a'"
  129. (string-join (cons first-word rest))) '())]
  130. [(list "JOIN" (colon target))
  131. ;; Alas, this pretty much never triggers, since duncanm keeps his client
  132. ;; session around for ever
  133. (when (regexp-match #rx"^duncanm" nick)
  134. (pm target "la la la"))
  135. (when (regexp-match #rx"^klutometis" nick)
  136. (pm target "\1ACTION bows deeply before his master, inventor of incubot\1"))
  137. (espy target
  138. (format "joining")
  139. '())]
  140. [(list "NICK" (colon new-nick))
  141. ;; TODO -- call espy with the old nick, or the new one, or both?
  142. (log "~a wants to be known as ~a" nick new-nick)]
  143. [(list "PART" target (colon first-word) rest ...)
  144. (espy target
  145. "leaving the channel"
  146. (cons first-word rest))]
  147. [(list "PRIVMSG"
  148. target
  149. (regexp #px"^:\u0001([[:alpha:]]+)" (list _ extended-data-word ))
  150. inner-words ...
  151. (regexp #px"(.*)\u0001$" (list _ trailing )))
  152. ((*incubot-server*) 'put (string-join (append inner-words (list trailing)) " "))
  153. (espy target
  154. (format "doing ~a: ~a" extended-data-word
  155. (string-join
  156. (append inner-words (list trailing))))
  157. '())]
  158. ;; Hard to see how this will ever match, given that the above clause
  159. ;; would seem to match VERSION
  160. [(list "PRIVMSG"
  161. target
  162. (regexp #px"^:\u0001(.*)\u0001" (list _ request-word ))
  163. rest ...)
  164. (log "request: ~s" request-word)
  165. (when (equal? "VERSION" request-word)
  166. (pm #:notice? #t
  167. nick
  168. "\u0001VERSION ~a (offby1@blarg.net):v4.~a:PLT scheme version ~a on ~a\0001"
  169. (unbox *my-nick*)
  170. (git-version)
  171. (version)
  172. (system-type 'os)))]
  173. [(list "PRIVMSG" target (colon first-word) rest ...)
  174. ;; Unspeakable hack -- "irc-process-line" is way too dumb, and
  175. ;; merely hands us whitespace-delimited tokens; it should
  176. ;; really have some knowledge of what IRC lines look like, and
  177. ;; split the line into semantically-meaningful units. But
  178. ;; until I get off my ass and do that properly ...
  179. ;; If first-word is just whitespace, then skip it. This
  180. ;; happens when someone types a line to their IRC client that
  181. ;; begins with whitespace.
  182. (when (and (not (null? rest))
  183. (regexp-match #px"^\\s*$" first-word))
  184. (set! first-word (car rest))
  185. (set! rest (cdr rest)))
  186. ;; fledermaus points out that people may be surprised
  187. ;; to find "private" messages -- those where "target"
  188. ;; is (unbox *my-nick*) -- recorded in the sightings log.
  189. (when (not (equal? target (unbox *my-nick*)))
  190. (espy target #f (cons first-word rest)))
  191. (cond
  192. [(regexp-match? #rx"[Bb]ot$" nick)
  193. (log "nick '~a' ends with 'bot', so I ain't gonna reply. Bot wars, you know."
  194. nick)]
  195. [(equal? target (unbox *my-nick*))
  196. (log "~a privately said ~a to me"
  197. nick (string-join (cons first-word rest)))
  198. (parameterize ([*full-id* full-id])
  199. (do-cmd nick nick (cons first-word rest) #:rate_limit? #f))]
  200. [else
  201. ;; look for long URLs to tiny-ify, but only if we're The
  202. ;; Original Rudybot, so avoid annoying duplicates from multiple
  203. ;; bots
  204. (when (regexp-match? #rx"^rudybot" (unbox *my-nick*))
  205. (for ([word (in-list (cons first-word rest))])
  206. (match word
  207. [(regexp url-regexp (list url _ _))
  208. (when (<= 75 (string-length url))
  209. (pm target "~a" (make-tiny-url url)))]
  210. [_ #f])))
  211. (when (and (regexp-match? #rx"^(?i:let(')?s)" first-word)
  212. (regexp-match? #rx"^(?i:jordanb)" nick))
  213. (log "KOMEDY GOLD: ~s" (cons first-word rest)))
  214. (match first-word
  215. ;; TODO -- use a regex that matches just those characters that
  216. ;; are legal in nicknames, followed by _any_ non-white
  217. ;; character -- that way people can use, say, a semicolon after
  218. ;; our nick, rather than just the comma and colon I've
  219. ;; hard-coded here.
  220. [(regexp #px"^([[:alnum:]_-]+)[,:](.*)" (list _ addressee garbage))
  221. (let ([words (if (positive? (string-length garbage))
  222. (cons garbage rest)
  223. rest)])
  224. (if (and (not (null? words))
  225. (equal? addressee (unbox *my-nick*)))
  226. (parameterize ([*full-id* full-id])
  227. (do-cmd target nick words #:rate_limit?
  228. (and #f
  229. (not (regexp-match #rx"^offby1" nick))
  230. (equal? target "#emacs" ))))
  231. ((*incubot-server*) 'put (string-join (cons first-word rest) " "))))]
  232. [(regexp #px",+\\.+")
  233. (when (equal? target "#emacs")
  234. (pm target "Woof."))]
  235. [_
  236. ((*incubot-server*) 'put (string-join (cons first-word rest) " "))
  237. ])])]
  238. [(list "QUIT" (colon first-word) rest ...)
  239. (espy host "quitting"
  240. (cons first-word rest))]
  241. [_ (log "~a said ~s, which I don't understand" nick
  242. (text-from-word (*current-words*)))])))
  243. (defmatcher IRC-COMMAND (colon host)
  244. (match (*current-words*)
  245. ;; ircd-seven (http://freenode.net/seven.shtml) emits this as soon
  246. ;; as we connect
  247. [(list "NOTICE" blather ...)
  248. (send-NICK-and-USER)]
  249. [(list digits mynick blather ...)
  250. (case (string->number digits)
  251. [(1)
  252. (log "Yay, we're in")
  253. (set-box! *authentication-state* 'succeeded)
  254. ;; BUGBUG --this appears to be to soon to join. _Most_
  255. ;; channels will let us join now; but some require that we
  256. ;; authenticate with nickserv first, and at this point, even
  257. ;; though we've already sent our password via
  258. ;; send-NICK-and-USER, Nickserv hasn't processed it. We thus
  259. ;; need to wait until Nickserv has acknowledged our attempt to
  260. ;; authenticate..
  261. ;; ":NickServ!NickServ@services. NOTICE rudybot :You are now identified for \u0002rudebot\u0002."
  262. (for ([c (*initial-channels*)]) (out "JOIN ~a" c))]
  263. [(366)
  264. (log "I, ~a, seem to have joined channel ~a."
  265. mynick
  266. (car blather))]
  267. [(433)
  268. (log "Nuts, gotta try a different nick")
  269. (set-box! *my-nick* (string-append (unbox *my-nick*) "_"))
  270. (out "NICK ~a" (unbox *my-nick*))])]
  271. [(list)
  272. (log "Completely unparseable line from the server. current-words ~s; host ~s"
  273. (*current-words*)
  274. host)]))
  275. (defmatcher IRC-COMMAND _ (log "Duh?"))
  276. ;; ----------------------------------------------------------------------------
  277. ;; User verbs
  278. (define verbs (make-hasheq))
  279. (define master-verbs (make-hasheq))
  280. (define verb-lines '())
  281. (define master-verb-lines '())
  282. (define hidden-verb-lines '())
  283. (require (for-syntax (only-in scheme last drop-right)))
  284. (define-syntax (*defverb stx)
  285. (define (id->str id) (and (identifier? id) (symbol->string (syntax-e id))))
  286. (syntax-case stx ()
  287. [(_ verbs verb-lines (verb arg ...) desc body ...)
  288. (and (identifier? #'verb)
  289. (andmap (lambda (s) (or (identifier? s) (string? (syntax-e s))))
  290. (syntax->list #'(arg ...)))
  291. (string? (syntax-e #'desc)))
  292. (let* ([raw-args (syntax->list #'(arg ...))]
  293. [args (map id->str raw-args)]
  294. [formstr
  295. (apply string-append
  296. (id->str #'verb)
  297. (map (lambda (a r)
  298. (cond [(not a) (format " ~s" (syntax-e r))]
  299. [(equal? a "...") " ..."]
  300. [(regexp-match? #rx"^[?]" a)
  301. (string-append " [<" (substring a 1) ">]")]
  302. [else (string-append " <" a ">")]))
  303. args raw-args))])
  304. (define clause
  305. (if (and (pair? args) (last args) (regexp-match? #rx"^[?]" (last args)))
  306. (let* ([opt (last raw-args)]
  307. [raw-args (drop-right raw-args 1)])
  308. #`[(list* #,@raw-args (and #,opt (or (list _) '())))
  309. (let ([#,opt (and (pair? #,opt) (car #,opt))]) body ...)])
  310. #'[(list arg ...) body ...]))
  311. #`(begin (hash-set! verbs 'verb
  312. (match-lambda #,clause
  313. [_ (reply "expecting: ~a" #,formstr)]))
  314. (set! verb-lines (cons '(verb #,formstr desc) verb-lines))))]))
  315. ;; defverb defines a new verb:
  316. ;; (defverb (verb arg ...) "description" body ...)
  317. ;; where `arg ...' can use `...' to get a "rest" argument, or one
  318. ;; `?id' for an optional argument.
  319. ;; In addition, it can have up to two flags:
  320. ;; - #:whine can appear, which will wrap the verb action in a call/whine
  321. ;; - #:master means that this is a master-only verb, or
  322. ;; #:hidden which makes it available for everyone, but not included in the
  323. ;; `help' output for a non-master user.
  324. (define-syntax (defverb stx)
  325. (let*-values ([(whine rest)
  326. (syntax-case stx ()
  327. [(_ #:whine . rest)
  328. (values #t #'rest)]
  329. [(_ kwd #:whine . rest)
  330. (keyword? (syntax-e #'kwd))
  331. (values #t #'(kwd . rest))]
  332. [(_ . rest) (values #f #'rest)])]
  333. [(verbs verb-lines rest)
  334. (syntax-case rest ()
  335. [(#:master . rest)
  336. (values #'master-verbs #'master-verb-lines #'rest)]
  337. [(#:hidden . rest)
  338. (values #'verbs #'hidden-verb-lines #'rest)]
  339. [_ (values #'verbs #'verb-lines rest)])])
  340. (syntax-case rest ()
  341. [((verb arg ...) desc body ...)
  342. (and (identifier? #'verb)
  343. (andmap (lambda (s) (or (identifier? s) (string? (syntax-e s))))
  344. (syntax->list #'(arg ...)))
  345. (string? (syntax-e #'desc)))
  346. #`(*defverb #,verbs #,verb-lines (verb arg ...) desc
  347. #,@(if whine #'((call/whine (lambda () body ...))) #'(body ...)))]
  348. [_ (raise-syntax-error 'defverb "malformed defverb" stx)])))
  349. (define (reply fmt . args)
  350. (let* ([response-target (*response-target*)]
  351. [for-whom (*for-whom*)]
  352. [response-prefix (if (equal? response-target for-whom)
  353. (if (is-master?) "* " "")
  354. (format (if (is-master?) "*~a: " "~a: ")
  355. for-whom))])
  356. (pm response-target "~a~a" response-prefix (apply format fmt args))))
  357. ;; ----------------------------------------------------------------------------
  358. ;; Misc utilities
  359. (defverb (help ?what) "what tricks can I do?"
  360. (let ([what (and ?what (string->symbol ?what))]
  361. [master? (is-master?)])
  362. (cond
  363. [(or (assq what verb-lines) (and master? (assq what master-verb-lines)))
  364. => (lambda (v) (reply "~a: ~a" (cadr v) (caddr v)))]
  365. [else (reply "~a"
  366. (string-join
  367. (map cadr (reverse `(,@(if master? master-verb-lines '())
  368. ,@verb-lines)))
  369. ", "))])))
  370. (defverb (version) "my source code version"
  371. (reply "~a" (git-version)))
  372. (defverb (quote) "words of wisdom"
  373. (let ([q (one-quote)])
  374. ;; special case: jordanb doesn't want quotes prefixed with his nick.
  375. (match (*for-whom*)
  376. [(regexp #rx"^jordanb") (pm (*response-target*) "~a" q)]
  377. [_ (reply "~a" q)])))
  378. (defverb (source) "my source location"
  379. (reply "git clone git://github.com/offby1/rudybot.git"))
  380. (defverb (seen nick) "did I see someone?"
  381. ;; TODO -- if "nick" is fsbot, and the last time we saw 'em was
  382. ;; quite a while ago, suggest /msg fledermaus (fsbot's owner).
  383. ;; Thanks to jlf for the idea.
  384. (reply "~a" (nick->sighting-string nick)))
  385. (defverb (uptime) "how long was I awake"
  386. (reply "I've been up for ~a; this tcp/ip connection has been up for ~a"
  387. (describe-since *start-time*)
  388. (describe-since (*connection-start-time*))))
  389. (defverb #:whine (t8 from to text ...) "translate TEXT from FROM to TO"
  390. (reply (xlate from to (string-join text " "))))
  391. (defverb #:hidden (ping) "am I alive?"
  392. (reply "pong"))
  393. ;; ----------------------------------------------------------------------------
  394. ;; Evaluation related stuffs
  395. (define *default-sandbox-language* 'racket)
  396. (define (call/whine f . args)
  397. (define (on-error e)
  398. (let ([whine (if (exn? e) (exn-message e) (format "~s" e))])
  399. (reply ;; make sure our error message begins with "error: ".
  400. (if (regexp-match? #rx"^error: " whine) "~a" "error: ~a")
  401. whine)))
  402. (with-handlers ([void on-error]) (apply f args)))
  403. (define (get-sandbox [force? #f])
  404. (let* ([for-whom (*for-whom*)]
  405. [lang (userinfo-ref for-whom 'sandbox-lang *default-sandbox-language*)]
  406. [lang-to-report (and (not (equal? lang *default-sandbox-language*))
  407. lang)]
  408. [lang (case lang
  409. [(r5rs) '(special r5rs)]
  410. [else lang])]
  411. [force/new? (or force? (box #f))]
  412. [sb (get-sandbox-by-name *sandboxes* for-whom lang force/new?)])
  413. (when (or force? (unbox force/new?))
  414. (if lang-to-report
  415. (reply "your ~s sandbox is ready" lang-to-report)
  416. (reply "your sandbox is ready")))
  417. sb))
  418. (define (do-eval words give-to)
  419. (define for-whom (*for-whom*))
  420. (define response-target (*response-target*))
  421. ;; catch _all_ exceptions from the sandbox, to prevent "eval (raise 1)" or
  422. ;; any other error from killing this thread (including creating the sandbox).
  423. (when give-to
  424. (cond [(equal? give-to (unbox *my-nick*)) (error "I'm full, thanks.")]
  425. [(equal? give-to for-whom)
  426. ;; allowing giving a value to yourself can lead to a nested call
  427. ;; to `call-in-sandbox-context' which will deadlock.
  428. (error "Talk to yourself much too?")]))
  429. (let ([s (get-sandbox)])
  430. (call-with-values (lambda () (sandbox-eval s (text-from-word words)))
  431. (lambda values
  432. ;; Even though the sandbox runs with strict memory and time limits, we
  433. ;; use call-with-limits here anyway, because it's possible that the
  434. ;; sandbox can, without exceeding its limits, return a value that will
  435. ;; require a lot of time and memory to convert into a string!
  436. ;; (make-list 100000) is an example.
  437. (call-with-limits 10 20 ; 10sec, 20mb
  438. (lambda ()
  439. (define (display-values values displayed shown?)
  440. (define (next s?)
  441. (display-values (cdr values) (add1 displayed) s?))
  442. (cond [(null? values) shown?]
  443. [(void? (car values)) (next shown?)]
  444. ;; prevent flooding
  445. [(>= displayed *max-values-to-display*)
  446. (reply
  447. "; ~a values is enough for anybody; here's the rest in a list: ~s"
  448. (number->english *max-values-to-display*)
  449. (filter (lambda (x) (not (void? x))) values))
  450. #t]
  451. [else (reply "; Value~a: ~s"
  452. (if (positive? displayed)
  453. (format "#~a" (add1 displayed))
  454. "")
  455. (car values))
  456. ;; another precaution against flooding.
  457. (sleep 1)
  458. (next #t)]))
  459. (define (display-values/give)
  460. (cond [(not give-to) (display-values values 0 #f)]
  461. [(null? values)
  462. (error "no value to give")]
  463. [(not (null? (cdr values)))
  464. (error "you can only give one value")]
  465. [else
  466. (sandbox-give s give-to (car values))
  467. ;; BUGBUG -- we shouldn't put "my-nick" in the
  468. ;; string if we're talking to a nick, as opposed to
  469. ;; a channel.
  470. (let* ((msg* (format "has given you a value, say \"~a: eval (GRAB)\""
  471. (unbox *my-nick*)))
  472. (msg (string-append msg* " to get it (case sensitive)")))
  473. (if (not (regexp-match? #rx"^#" response-target))
  474. ;; announce privately if given privately
  475. (pm give-to "~a ~a" for-whom msg)
  476. ;; cheap no-nag feature
  477. (let* ((l last-give-instructions)
  478. (msg (if (and l
  479. (equal? (car l) response-target)
  480. (< (- (current-seconds) (cdr l)) 120))
  481. msg*
  482. msg)))
  483. (set! last-give-instructions
  484. (cons response-target (current-seconds)))
  485. (pm response-target
  486. "~a: ~a ~a" give-to for-whom msg))))
  487. #t])) ; said something
  488. (define (display-output name output-getter)
  489. (let ([output (output-getter s)])
  490. (and (string? output) (positive? (string-length output))
  491. (begin (reply "; ~a: ~s" name output) (sleep 1) #t))))
  492. (unless (or (display-values/give)
  493. (display-output 'stdout sandbox-get-stdout)
  494. (display-output 'stderr sandbox-get-stderr))
  495. (reply "Done."))))))))
  496. (defverb #:whine (init ?lang)
  497. "initialize a sandbox, <lang> can be 'r5rs, 'scheme, 'scheme/base, etc"
  498. (when ?lang
  499. (userinfo-set! (*for-whom*) 'sandbox-lang
  500. (if (regexp-match? #rx"^http://" ?lang)
  501. ?lang (string->symbol ?lang))))
  502. (get-sandbox #t))
  503. (defverb #:whine (eval expr ...) "evaluate an expression(s)"
  504. (do-eval expr #f))
  505. (defverb #:whine (give to expr ...) "evaluate and give someone the result"
  506. (do-eval expr to))
  507. (defautoloads
  508. [net/uri-codec uri-encode]
  509. [setup/private/path-utils path->name]
  510. [setup/xref load-collections-xref]
  511. [setup/dirs find-doc-dir]
  512. [scribble/xref xref-binding->definition-tag xref-tag->path+anchor
  513. xref-index entry-desc])
  514. ;; these cannot be autoloaded, since it leads to some problem with errortrace
  515. (require (only-in scribble/manual-struct
  516. exported-index-desc? exported-index-desc-name
  517. exported-index-desc-from-libs))
  518. (define (binding-info id-str)
  519. (call-in-sandbox-context (sandbox-evaluator (get-sandbox))
  520. (lambda ()
  521. (let* ([sym (string->symbol id-str)]
  522. [id (namespace-symbol->identifier sym)])
  523. (values sym id (identifier-binding id))))))
  524. ;; Based on Eli's interactive library
  525. (defverb #:whine (apropos str ...) "look for a binding"
  526. (if (null? str)
  527. (reply "give me something to look for")
  528. (let* ([arg (map (compose regexp regexp-quote) str)]
  529. [arg (lambda (str)
  530. (andmap (lambda (rx) (regexp-match? rx str)) arg))]
  531. [syms (namespace-mapped-symbols
  532. (call-in-sandbox-context (sandbox-evaluator (get-sandbox))
  533. current-namespace))]
  534. [syms (filter-map (lambda (sym)
  535. (let ([str (symbol->string sym)])
  536. (and (arg str) str)))
  537. syms)]
  538. [syms (sort syms string<?)])
  539. (if (null? syms)
  540. (reply "no matches found")
  541. (reply "matches: ~a." (string-join syms ", "))))))
  542. ;; Based on Eli's interactive library
  543. (defverb #:whine (desc id) "describe an identifier"
  544. (define-values (sym identifier info) (binding-info id))
  545. (cond
  546. [(not info) (reply "`~s' is a toplevel (or unbound) identifier" sym)]
  547. [(eq? info 'lexical) (reply "`~s' is a lexical identifier" sym)]
  548. [(or (not (list? info)) (not (= 7 (length info))))
  549. (error "internal error, racket changed on me")]
  550. [else
  551. (let-values ([(source-mod source-id
  552. nominal-source-mod nominal-source-id
  553. source-phase import-phase
  554. nominal-export-phase)
  555. (apply values info)])
  556. (define (mpi* mpi)
  557. (let ([p (resolved-module-path-name
  558. (module-path-index-resolve mpi))])
  559. (if (path? p) (path->name p) p)))
  560. (let ([source-mod (mpi* source-mod)]
  561. [nominal-source-mod (mpi* nominal-source-mod)])
  562. (reply
  563. "~a"
  564. (string-append*
  565. `("`",id"' is a bound identifier,"
  566. " defined"
  567. ,(case source-phase
  568. [(0) ""] [(1) "-for-syntax"] [else (error "internal error")])
  569. " in \"",(format "~a" source-mod)"\""
  570. ,(if (not (eq? sym source-id))
  571. (format " as `~s'" source-id)
  572. "")
  573. " required"
  574. ,(case import-phase
  575. [(0) ""] [(1) "-for-syntax"] [else (error "internal error")])
  576. " "
  577. ,(if (equal? source-mod nominal-source-mod)
  578. "directly"
  579. (format "through \"~a\"~a"
  580. nominal-source-mod
  581. (if (not (eq? sym nominal-source-id))
  582. (format " where it is defined as `~s'"
  583. nominal-source-id)
  584. "")))
  585. ,(case nominal-export-phase
  586. [(0) ""] [(1) (format ", (exported-for-syntax)")]
  587. [else (error "internal error")]))))))]))
  588. ;; Based on help/help-utils
  589. (define remove-doc-dir
  590. (regexp (string-append "^" (regexp-quote (path->string (find-doc-dir))) "/")))
  591. (define doc-url "http://docs.racket-lang.org/")
  592. (defverb #:whine (doc id) "find documentation for a binding"
  593. (define-values (sym identifier info) (binding-info id))
  594. (define xref
  595. (load-collections-xref (lambda () (log "Loading help index..."))))
  596. (if info
  597. (let ([tag (xref-binding->definition-tag xref info 0)])
  598. (if tag
  599. (let*-values ([(file anchor) (xref-tag->path+anchor xref tag)]
  600. [(file) (path->string file)]
  601. [(m) (regexp-match-positions remove-doc-dir file)]
  602. [(url) (and m (string-append
  603. doc-url
  604. (substring file (cdar m))))]
  605. [(url) (cond [(and anchor url)
  606. (string-append url "#" (uri-encode anchor))]
  607. [url url]
  608. [else "??hidden??"])])
  609. (reply "~a" url))
  610. (error 'help
  611. "no documentation found for: ~e provided by: ~a"
  612. sym
  613. (module-path-index-resolve (caddr info)))))
  614. (search-for-exports xref sym)))
  615. (define (search-for-exports xref sym)
  616. (let ([idx (xref-index xref)]
  617. [libs null])
  618. (for ([entry (in-list idx)])
  619. (when (and (exported-index-desc? (entry-desc entry))
  620. (eq? sym (exported-index-desc-name (entry-desc entry))))
  621. (set! libs (append libs (exported-index-desc-from-libs
  622. (entry-desc entry))))))
  623. (if (null? libs)
  624. (reply "not found in any library's documentation: ~a" sym)
  625. (reply "no docs for a current binding, but provided by: ~a"
  626. (string-join (map symbol->string (remove-duplicates libs))
  627. ", ")))))
  628. ;; Silly stuffs
  629. (define-syntax-rule (defspecbotverbs db ...)
  630. (begin (defverb #:hidden (db term (... ...)) "look something up"
  631. (pm (*response-target*) "specbot: ~a ~a" 'db (string-join term)))
  632. ...))
  633. (defspecbotverbs
  634. db clhs r5rs cocoa elisp clim ieee754 ppc posix man cltl2 cltl2-section)
  635. (define-syntax-rule (defminionverbs verb ...)
  636. (begin (defverb #:hidden (verb stuff (... ...)) "do some minion work"
  637. (pm (*response-target*) "minion: ~a ~a" 'verb (string-join stuff)))
  638. ...))
  639. (defminionverbs chant advice memo)
  640. (defverb (later "tell" nick something ...) "leave a message for someone"
  641. (pm (*response-target*) "minion: memo for ~a: ~a told me to tell you: ~a"
  642. nick (*for-whom*) (string-join something)))
  643. ;; ----------------------------------------------------------------------------
  644. ;; Master tools
  645. (define *master-password* #f)
  646. (defverb #:hidden (authenticate ?passwd) "request a passwd, or use one"
  647. (cond [(not ?passwd)
  648. (let ([passwd (random 1000000000)])
  649. (set! *master-password* (cons (current-seconds) passwd))
  650. (log "--->>> Temporary password: ~a <<<---" passwd)
  651. (pm (*for-whom*) "Check the logs."))]
  652. [(not *master-password*)
  653. (reply "No password set")]
  654. [(> (- (current-seconds) (car *master-password*)) 120)
  655. (reply "Too late, generate a new password")]
  656. [(not (equal? (string->number ?passwd) (cdr *master-password*)))
  657. ;; avoid brute force attacks however unlikely
  658. (set! *master-password* #f)
  659. (log "Bad authentication attempt!")
  660. (reply "Bad password, generate a new one now")]
  661. [else
  662. (set-box! *my-master* (*full-id*))
  663. ;; in case the password was uttered publicly, avoid hijacks it
  664. (set! *master-password* #f)
  665. (log "I am a mindless puppet")
  666. (reply "[bows deeply] Welcome, oh great master!")]))
  667. (defverb #:master (join channel) "ask me to join a channel"
  668. (if (regexp-match? #rx"^#" channel)
  669. (begin (out "JOIN ~a" channel) (reply "OK"))
  670. (reply "not a proper channel name")))
  671. (defverb #:master (part channel) "ask me to part from a channel"
  672. (if (regexp-match? #rx"^#" channel)
  673. (begin (out "PART ~a" channel) (reply "OK"))
  674. (reply "not a proper channel name")))
  675. (defverb #:master (tell who stuff ...) "tell me to tell someone something"
  676. (pm (*response-target*) "~a: ~a" who (string-join stuff)))
  677. (defverb #:master (emote stuff ...) "tell me to do something"
  678. (pm (*response-target*) "\1ACTION ~a\1" (string-join stuff)))
  679. (defverb #:master (for who stuff ...) "tell me something in someone's name"
  680. (parameterize ([*full-id* ""]) ; avoid allowing master commands
  681. (do-cmd (*response-target*) who stuff)))
  682. (defverb #:master (ghost victim) "kill an errant client that's using my favorite nick"
  683. (pm "NickServ" (format "ghost ~a ~a" victim (*nickserv-password*))))
  684. (defverb #:master (nick new-nick) "tell me to rename myself"
  685. (out "NICK ~a" new-nick))
  686. (defverb #:master (system command ...) "run something"
  687. (let ([s (open-output-string)])
  688. (parameterize ([current-output-port s] [current-error-port s])
  689. (call-with-PATH (lambda () (call/whine system (string-join command)))))
  690. (let* ([s (get-output-string s)]
  691. [s (regexp-replace #rx"^[ \r\n]+" s "")]
  692. [s (regexp-replace #rx"[ \r\n]+$" s "")]
  693. [s (regexp-replace* #rx" *[\r\n] *" s " <NL> ")])
  694. (reply "~a" (if (equal? s "") "OK" s)))))
  695. (define-namespace-anchor anchor)
  696. (define my-namespace (namespace-anchor->namespace anchor))
  697. (defverb #:master (top-eval expr ...) "evaluate something in the sandbox"
  698. (call/whine
  699. (lambda ()
  700. (reply "~s"
  701. (eval (read (open-input-string
  702. (string-append "(begin " (string-join expr) ")")))
  703. my-namespace)))))
  704. ;; ----------------------------------------------------------------------------
  705. ;; Incubot-like
  706. (define (get-incubot-witticism words)
  707. (define incubot-witticism ((*incubot-server*) 'get words))
  708. (define (strip-just-one rx) (curryr (curry regexp-replace rx) ""))
  709. (define (trim-ACTION str)
  710. (regexp-replace #rx"\1ACTION (.*)\1" str "\\1"))
  711. (define (trim-leading-nick str)
  712. (if (regexp-match #px"^http(s)?://" str)
  713. str
  714. ((strip-just-one #px"^\\w+?: *") str)))
  715. (and incubot-witticism
  716. (lambda ()
  717. (reply "~a" (trim-ACTION (trim-leading-nick incubot-witticism))))))
  718. ;; ----------------------------------------------------------------------------
  719. ;; Main dispatchers
  720. (define (do-cmd response-target for-whom words #:rate_limit? [rate_limit? #f])
  721. (parameterize ([*for-whom* for-whom]
  722. [*response-target* response-target])
  723. (if (and rate_limit? (we-recently-did-something-for for-whom))
  724. (log "Not doing anything for ~a, since we recently did something for them."
  725. for-whom)
  726. (let loop ([words words]
  727. [verb (string->symbol (string-downcase (car words)))])
  728. (cond [(or (hash-ref verbs verb #f)
  729. (and (is-master?) (hash-ref master-verbs verb #f)))
  730. => (lambda (p)
  731. (log "Doing for ~a: ~a ~s" for-whom verb (cdr words))
  732. (p (cdr words)))]
  733. [(roughly-evaluable? for-whom (text-from-word words))
  734. (loop (cons "eval" words) 'eval)]
  735. [(get-incubot-witticism words)
  736. => (lambda (p)
  737. (log "Spewing wisdom for ~a re ~a"
  738. for-whom (text-from-word words))
  739. (p))]
  740. [else (log "Not doing for ~a: ~a" for-whom (text-from-word words))
  741. (reply "eh? Try \"~a: help\"." (unbox *my-nick*))])
  742. (note-we-did-something-for! for-whom)))))
  743. ;; Maps a word to a (text . (start . end)) position in the original text
  744. ;; (store the text to avoid worrying about a global `*current-line*'
  745. ;; thing that can disappear)
  746. (define word-posns (make-weak-hasheq))
  747. ;; Utility to get text from a word to the end (also accepts a list, and
  748. ;; will use the first word); `emergency' is some value to use in case we
  749. ;; don't find the text
  750. (define (text-from-word w [emergency w])
  751. (cond [(pair? w) (text-from-word (car w) emergency)]
  752. [(not (string? w))
  753. (error 'text-from-word "Bad code, I have a bug -- got: ~s" w)]
  754. [else (cond [(hash-ref word-posns w #f)
  755. => (lambda (p) (substring (car p) (cadr p)))]
  756. [(string? emergency) emergency]
  757. [(list? emergency) (string-join emergency " ")]
  758. [else (format "~a" emergency)])]))
  759. ;; Sometimes we need to take a substring of a word -- so use this to
  760. ;; register its info too
  761. (define (substring+posn str b [e (string-length str)])
  762. (let ([p (hash-ref word-posns str #f)]
  763. [r (substring str b e)])
  764. (when p
  765. (hash-set! word-posns r
  766. (cons (car p) (cons (+ (cadr p) b) (+ (cadr p) e)))))
  767. r))
  768. (define rx:word #px"(?:\\p{L}+|\\p{N}+|\\p{S}|\\p{P})+")
  769. (define (irc-process-line line)
  770. (let* ([posns (regexp-match-positions* rx:word line)]
  771. [words (map (lambda (p)
  772. (let ([s (substring line (car p) (cdr p))])
  773. (hash-set! word-posns s (cons line p))
  774. s))
  775. posns)])
  776. (when (null? words) (log "BAD IRC LINE: ~a" line))
  777. (parameterize ([*current-words* (cdr words)])
  778. (domatchers IRC-COMMAND (car words)))))