Fixed line buffering, added example.
[botbot.git] / botbot.scm
index 18ad842..a03ea80 100644 (file)
@@ -1,4 +1,19 @@
 ;; Botbot: Very basic IRC bot
+;;
+;; Copyright (C) 2023 plugd
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 (import (chicken io)
         (chicken port)
         (chicken string)
         (chicken pathname)
         (chicken process-context)
+        (chicken condition)
         (chicken irregex)
-        matchable srfi-13 srfi-1
-        uri-common tcp6 openssl)
+        matchable srfi-13 srfi-1 srfi-18
+        tcp6 openssl)
 
 ;; Globals
 
-(define nick "botbot")
-(define username "#phloggersgarage bot")
+(define irc-host #f)
+(define irc-port #f)
+(define bot-nick #f)
+(define bot-channel #f)
+(define bot-proc-file #f)
+(define usetls #t)
+(define allow-reload #f)
+
+(define bot-proc #f)
+
+(define verbosity-level 0)
+
+(define ping-period 60) ;seconds
 
 (tcp-read-timeout #f) ;disable read timeout
 
-(define (launch-bot host port)
-  (let-values (((in-port out-port) (tcp-connect host port)))
+(define (launch-bot)
+  ;; (let-values (((in-port out-port) (tcp-connect host port)))
+  (set-buffering-mode! (current-output-port) #:line)
+  (let-values (((in-port out-port)
+                (if usetls
+                    (ssl-connect* hostname: irc-host port: (or irc-port 6697))
+                    (tcp-connect irc-host (or irc-port 6667)))))
     ;; Connect to server
     (if (establish-connection in-port out-port)
         ;; (bot-loop in-port out-port)
         (begin
           (print "Successfully connected!")
+          (start-ping-timer out-port)
           (bot-loop in-port out-port))
         (print "Failed to establish connection. Aborting..."))))
 
 (define (establish-connection in-port out-port)
-  (write-msg `(#f #f "NICK" (,nick)) out-port)
-  (write-msg `(#f #f "USER" (,nick "0" "*" ,username)) out-port)
-  (let ((msg (read-msg in-port)))
-    (print msg)
-    (string=? (msg-command msg) "001")))
+  (write-msg `(#f #f "NICK" (,bot-nick)) out-port)
+  (write-msg `(#f #f "USER" (,bot-nick "0" "*" ,bot-nick)) out-port)
+  (if bot-channel
+      (write-msg `(#f #f "JOIN" (,bot-channel)) out-port))
+  #t)
+
+(define (start-ping-timer out-port)
+  (thread-start!
+   (lambda ()
+     (let loop ()
+       (thread-sleep! ping-period)
+       (write-msg `(#f #f "PING" (,irc-host)) out-port) ; send ping
+       (loop)))))
+
+(define (load-bot)
+  (let ((new-bot-proc
+         (condition-case
+             (eval (with-input-from-file bot-proc-file read))
+           (o (exn)
+              (print-error-message o)
+              #f))))
+    (if new-bot-proc
+        (begin
+          (set! bot-proc new-bot-proc)
+          (print "Loaded bot procedure file."))
+        (print "Error loading procedure file."))
+    new-bot-proc))
 
 (define (bot-loop in-port out-port)
-  (let loop ((msg (read-msg in-port)))
-    (match (cons (msg-source msg) (cons (msg-command msg) (msg-args msg)))
-      ((_ "PING" token)
-       (write-msg `(#f #f "PONG" (,token)) out-port))
-      ((source "PRIVMSG" target args ...)
-       (when (string=? target nick)
-         (print "Someone sent me this message: " args)
-         (write-msg `(#f #f "PRIVMSG" (,source "Message received!")) out-port)))
-      (_
-       ; Do nothing
-       ))
-    (loop (read-msg in-port))))
+  (let ((privmsg (lambda (to . args)
+                   (let ((msg (list #f #f "PRIVMSG" (cons to args))))
+                     (write-msg msg out-port)
+                     (when (>= verbosity-level 1)
+                       (display "Responded with msg: ")
+                       (write msg)
+                       (newline))))))
+    (let loop ((msg (read-msg in-port)))
+      (match (cons (msg-source msg) (cons (msg-command msg) (msg-args msg)))
+        ((_ "PING" token)
+         (write-msg `(#f #f "PONG" (,token)) out-port))
+        ((source "PRIVMSG" target "bbreload")
+         (when (and allow-reload (string=? target bot-nick))
+           (print "Reveived reload command from " source)
+           (if (load-bot)
+               (privmsg source "Reloaded bot script.")
+               (privmsg source "Error loading bot script."))))
+        ((source "PRIVMSG" target args ...)
+         (when (string=? target bot-nick)
+           (when (>= verbosity-level 1)
+             (display "Received msg: ")
+             (write msg)
+             (newline))
+           (condition-case
+               (bot-proc source args privmsg)
+             (o (exn)
+                (print "Error executing bot script.")
+                (print-error-message o)))))
+        (_
+         ;; Do nothing
+         ))
+      (loop (read-msg in-port)))))
 
 (define (read-msg in-port)
   (let ((msg (string->msg (read-line in-port))))
-    (print "Received message: " msg)
+    (when (>= verbosity-level 2)
+      (display "Received message: ")
+      (write msg)
+      (newline))
     msg))
 
 (define (write-msg msg out-port)
   (with-output-to-port out-port
     (lambda () (write-string (conc (msg->string msg) "\r\n"))))
-  (print "Sent message: " msg))
+  (when (>= verbosity-level 2)
+    (print "Sent message: " msg)))
 
 (define msg-regex
   (irregex '(:
 
 (define (parse-message-args argstr)
   (if argstr
-      (let ((first-split (string-split argstr ":")))
-        (if (null? first-split)
-            #f
+      (let ((idx (substring-index ":" argstr)))
+        (if idx
             (append
-             (string-split (car first-split) " ")
-             (cdr first-split))))))
+             (string-split (substring argstr 0 idx) " ")
+             (list (substring argstr (+ idx 1))))
+            (string-split argstr " ")))))
 
 (define (msg-tags msg) (list-ref msg 0))
 (define (msg-source msg) (list-ref msg 1))
 (define (msg-args msg) (list-ref msg 3))
 
 (define (print-usage progname)
-  (print "Usage: " progname " host port"))
+  (let ((indent-str (make-string (string-length progname) #\space)))
+    (print "Usage:\n"
+           progname " [-h/--help]\n"
+           progname " [-p/--port PORT] [--notls] [-c/--channnel CHANNEL]\n"
+           indent-str " [-v/--verbose] [-a/--allow-reload]\n"
+           indent-str " proc-file host nick")))
 
 (define (main)
-  (let ((progname (pathname-file (car (argv)))))
-    (match (command-line-arguments)
-      ((host port)
-       (launch-bot host (string->number port)))
-      (_
-       (print-usage progname)))))
+  (let ((progname (pathname-file (car (argv))))
+        (port 6697)
+        (channel #f))
+    (if (null? (command-line-arguments))
+        (print-usage progname)
+        (let loop ((args (command-line-arguments)))
+          (let ((this-arg (car args))
+                (rest-args (cdr args)))
+            (if (string-prefix? "-" this-arg)
+                (cond
+                 ((or (equal? this-arg "-h")
+                      (equal? this-arg "--help"))
+                  (print-usage progname))
+                 ((or (equal? this-arg "-p")
+                      (equal? this-arg "--port"))
+                  (set! irc-port (string->number (car rest-args)))
+                  (loop (cdr rest-args)))
+                 ((equal? this-arg "--notls")
+                  (set! usetls #f)
+                  (loop rest-args))
+                 ((or (equal? this-arg "-c")
+                      (equal? this-arg "--channel"))
+                  (set! bot-channel (car rest-args))
+                  (loop (cdr rest-args)))
+                 ((or (equal? this-arg "-v")
+                      (equal? this-arg "--verbose"))
+                  (set! verbosity-level (+ 1 verbosity-level))
+                  (loop rest-args))
+                 ((or (equal? this-arg "-a")
+                      (equal? this-arg "--allow-reload"))
+                  (set! allow-reload #t)
+                  (loop rest-args))
+                 (else
+                  (print "Unknown argument '" this-arg "'")
+                  (print-usage progname)))
+                (match args
+                  ((procfile host nick)
+                   (set! bot-proc-file procfile)
+                   (set! irc-host host)
+                   (set! bot-nick nick)
+                   (if  (load-bot)
+                        (launch-bot)
+                        (error "Could not load bot procedure.")))
+                  (else
+                   (print "One or more invalid arguments.")
+                   (print-usage progname)))))))))
 
 (main)