X-Git-Url: https://jxself.org/git/?a=blobdiff_plain;f=8sync%2Fsystems%2Firc.scm;h=d13294c06ea10b200c1dbb226ee80be19be25679;hb=381c180279b47d0536f12da170c3911475af0755;hp=6262f43a249969326a0782857f228087ab048232;hpb=66ea38606d0d57f05a4ce49a94c770d17ce31fc3;p=8sync.git diff --git a/8sync/systems/irc.scm b/8sync/systems/irc.scm old mode 100755 new mode 100644 index 6262f43..d13294c --- a/8sync/systems/irc.scm +++ b/8sync/systems/irc.scm @@ -1,9 +1,6 @@ -#!/usr/bin/guile \ --e main -s -!# - ;;; 8sync --- Asynchronous programming for Guile -;;; Copyright (C) 2015 Christopher Allan Webber +;;; Copyright © 2015, 2016, 2017 Christopher Allan Webber +;;; Copyright © 2023 Janneke Nieuwenhuizen ;;; ;;; This file is part of 8sync. ;;; @@ -23,110 +20,180 @@ (define-module (8sync systems irc) #:use-module (8sync repl) #:use-module (8sync agenda) + #:use-module (8sync actors) + #:use-module (8sync contrib irc) #:use-module (srfi srfi-9) + #:use-module (srfi srfi-26) #:use-module (ice-9 getopt-long) #:use-module (ice-9 format) #:use-module (ice-9 receive) #:use-module (ice-9 rdelim) #:use-module (ice-9 q) #:use-module (ice-9 match) - #:export (;; The only things you definitely need if writing a bot - make-irc-bot-cli - irc-format irc-display irc-send-message irc-send-formatted - - ;; Useful things if you're making something more complicated - irc-line - irc-eol - - default-irc-port - - startswith-colon? - - - make-irc-line irc-line? - irc-line-prefix irc-line-command irc-line-params - - parse-line - irc-line-username - - condense-privmsg-line - echo-back-message - - make-handle-line make-basic-irc-handler - queue-and-start-irc-agenda!)) + #:use-module (oop goops) + #:export ( + irc-bot-username irc-bot-server irc-bot-channels irc-bot-port + + irc-bot-init irc-bot-cleanup + dispatch-message handle-message + + default-irc-port ;REMOVEME compat + irc-bot-send-line ;REMOVEME compat + handle-line handle-misc-input)) ;REMOVEME compat + +;;; A goofy default handler. +(define* (echo-message irc-bot speaker channel-name + line-text emote? #:key (port (current-output-port))) + "Simply echoes the message to the PORT." + (if emote? + (format port "~a emoted ~s in channel ~a\n" + speaker line-text channel-name) + (format port "~a said ~s in channel ~a\n" + speaker line-text channel-name))) -;;; Network stuff -;;; ============= - -(define default-irc-port 6665) - -(define* (irc-socket-setup hostname #:optional (inet-port default-irc-port)) - (let* ((s (socket PF_INET SOCK_STREAM 0)) - (flags (fcntl s F_GETFL)) - (ip-address (inet-ntop AF_INET (car (hostent:addr-list (gethost hostname)))))) - (fcntl s F_SETFL (logior O_NONBLOCK flags)) - (connect s AF_INET - (inet-pton AF_INET ip-address) - inet-port) - s)) - -(define irc-eol "\r\n") +;;; Bot +;;; === -(define (irc-line line) - (string-concatenate (list line irc-eol))) +(define-class () + (username #:init-keyword #:username + #:getter irc-bot-username) + (realname #:init-keyword #:realname + #:init-value #f) + (server #:init-keyword #:server + #:getter irc-bot-server) + (channels #:init-keyword #:channels + #:getter irc-bot-channels) + (port #:init-keyword #:port + #:init-value %irc:default-port + #:getter irc-bot-port) + (socket #:accessor irc-bot-socket) + (actions #:allocation #:each-subclass + #:init-thunk (build-actions + (*init* irc-bot-init) + (*cleanup* irc-bot-cleanup) + (main-loop irc-bot-main-loop) + (dispatch-message dispatch-message) + (handle-line handle-line) ;REMOVEME compat + (send-line irc-bot-send-line-action)))) + +(define (irc-bot-realname irc-bot) + (or (slot-ref irc-bot 'realname) + (irc-bot-username irc-bot))) + +(define-method (irc-bot-init (irc-bot ) message) + "Initialize the IRC bot" + (define socket (irc:listen (irc-bot-server irc-bot) + #:port (irc-bot-port irc-bot) + #:sleep 8sleep)) + (define flags (fcntl socket F_GETFL)) + + (fcntl socket F_SETFL (logior O_NONBLOCK flags)) + (set! (irc-bot-socket irc-bot) socket) + + (irc:user socket (irc-bot-username irc-bot) + #:real (irc-bot-realname irc-bot)) + (irc:nick socket (irc-bot-username irc-bot)) + + (for-each (cute irc:join socket <>) (irc-bot-channels irc-bot)) + + (<- (actor-id irc-bot) 'main-loop)) + +(define-method (irc-bot-cleanup (irc-bot ) message) + (close (irc-bot-socket irc-bot))) + +(define (irc-bot-main-loop irc-bot message) + (define socket (irc-bot-socket irc-bot)) + (define line (irc:receive socket)) + (define message (or (false-if-exception (irc:line->message line)) + line)) + (<- (actor-id irc-bot) 'dispatch-message message) + (cond + ;; The port's been closed for some reason, so stop looping + ((port-closed? socket) + 'done) + ;; We've reached the EOF object, which means we should close + ;; the port ourselves and stop looping + ((eof-object? (peek-char socket)) + (close socket) + 'done) + ;; ;; Looks like we've been killed somehow... well, stop running + ;; ;; then! + ;; ((actor-am-i-dead? irc-bot) + ;; (if (not (port-closed? socket)) + ;; (close socket)) + ;; 'done) + ;; Otherwise, let's read till the next line! + (else + (<- (actor-id irc-bot) 'main-loop)))) + +(define* (irc-bot-send-line-action irc-bot message + channel line #:key emote?) + "Action handler for sending lines. Real behavior happens in +irc:send-line." + (define socket (irc-bot-socket irc-bot)) + (irc:send-line socket channel line #:emote? emote?)) -(define-syntax-rule (irc-format dest format-string rest ...) - (let ((line (string-concatenate - (list (format #f format-string rest ...) - irc-eol)))) - (match dest - (#f line) - (#t (display line)) - (else - (display line dest))))) + +;;; +;;; Likely-to-be-overridden generic methods +;;; +(define-method (dispatch-message (irc-bot ) 8sync-message message) + "Dispatch an ." + (match message + ((and ($ ) + (= irc:message-command 'PING) + (= irc:message-message message)) + (irc:pong (irc-bot-socket irc-bot) message)) + (_ (handle-message irc-bot message)))) + +(define-method (handle-message (irc-bot ) message) + (match message + ((and ($ ) + (= irc:message-line line) + (= irc:message-command command) + (= irc:message-speaker speaker) + (= irc:message-channel channel) + (= irc:message-message message) + (= irc:message-emote? emote?)) + (or + (case command + ((PRIVMSG) + (handle-line irc-bot #f speaker channel message emote?)) ;REMOVEME compat + (else + (handle-misc-input irc-bot line))) ;REMOVEME compat + (echo-message irc-bot speaker channel message #f + #:port (current-error-port)))))) -(define* (irc-display line #:optional dest) - (if dest - (display (irc-line line) dest) - (display (irc-line dest)))) + +;;; +;;; Compatibility with 0.4.2. +;;; +(define default-irc-port %irc:default-port) +(define irc-eol %irc:eol) -(define (irc-send-message socket channel message) - (irc-format socket "PRIVMSG ~a :~a" channel message)) +(define* (irc-bot-send-line irc-bot channel line #:key emote?) + (define socket (irc-bot-socket irc-bot)) + (irc:send-line socket channel line)) -(define-syntax-rule (irc-send-formatted socket channel format-string - args ...) - (irc-format socket "PRIVMSG ~a :~a" channel - (format #f format-string args ...))) +(define-method (handle-line (irc-bot ) ;REMOVEME compat + 8sync-message + username channel-name line-text emote?) + "Keep compatibility with previous release." + #f) -(define* (handle-login socket username - #:key - (hostname "*") - (servername "*") - (realname username) - (channels '())) - (irc-format socket "USER ~a ~a ~a :~a" - username hostname servername realname) - (irc-format socket "NICK ~a" username) - (for-each - (lambda (channel) - (irc-format socket "JOIN ~a" channel)) - channels)) +(define-method (handle-misc-input (irc-bot ) ;REMOVEME compat + (line )) + "Keep compatibility with previous release." + #f) (define (startswith-colon? str) (and (> (string-length str) 0) (eq? (string-ref str 0) #\:))) -(define-record-type - (make-irc-line prefix command params) - irc-line? - (prefix irc-line-prefix) - (command irc-line-command) - (params irc-line-params)) - - -(define (parse-line line) +;; TODO: This needs a cleanup. Maybe even just using a regex is fine. +(define (parse-line line) ;REMOVEME compat (define (parse-params pre-params) ;; This is stupid and imperative but I can't wrap my brain around ;; the right way to do it in a functional way :\ @@ -156,29 +223,29 @@ (((? startswith-colon? prefix) command pre-params ...) - (make-irc-line prefix command - (parse-params pre-params))) + (values prefix command + (parse-params pre-params))) ((command pre-params ...) - (make-irc-line #f command - (parse-params pre-params))))) + (values #f command + (parse-params pre-params))))) -(define (strip-colon-if-necessary string) +(define (strip-colon-if-necessary string) ;REMOVME compat (if (and (> (string-length string) 0) (string-ref string 0)) (substring/copy string 1) string)) ;; @@: Not sure if this works in all cases, like what about in a non-privmsg one? -(define (irc-line-username irc-line) - (let* ((prefix-name (strip-colon-if-necessary (irc-line-prefix irc-line))) +(define (irc-line-username irc-line-prefix) ;REMOVME compat + (let* ((prefix-name (strip-colon-if-necessary irc-line-prefix)) (exclaim-index (string-index prefix-name #\!))) (if exclaim-index (substring/copy prefix-name 0 exclaim-index) prefix-name))) -(define (condense-privmsg-line line) +(define (condense-privmsg-line line) ;REMOVME compat "Condense message line and do multiple value return of - (channel message is-action)" + (channel message emote?)" (define (strip-last-char string) (substring/copy string 0 (- (string-length string) 1))) (let* ((channel-name (caar line)) @@ -194,113 +261,3 @@ (values channel-name (string-join (cons first-word rest-message) " ") #f))))) - -(define (echo-back-message socket my-name speaker - channel-name message is-action) - (if is-action - (format #t "~a emoted ~s in channel ~a\n" - speaker message channel-name) - (format #t "~a said ~s in channel ~a\n" - speaker message channel-name))) - -(define default-handle-privmsg echo-back-message) - -(define* (make-handle-line #:key - (handle-privmsg default-handle-privmsg)) - (define (handle-line socket line my-username) - (let ((parsed-line (parse-line line))) - (match (irc-line-command parsed-line) - ("PING" - (irc-display "PONG" socket)) - ("PRIVMSG" - (receive (channel-name message is-action) - (condense-privmsg-line (irc-line-params parsed-line)) - (let ((username (irc-line-username parsed-line))) - (handle-privmsg socket my-username username - channel-name message is-action)))) - (_ - (display line) - (newline))))) - handle-line) - -(define (irc-loop socket handle-line username) - (define (loop) - (define line (string-trim-right (read-line socket) #\return)) - (handle-line socket line username) - (cond - ;; The port's been closed for some reason, so stop looping - ((port-closed? socket) - 'done) - ;; We've reached the EOF object, which means we should close - ;; the port ourselves and stop looping - ((eof-object? (peek-char socket)) - (close socket) - 'done) - ;; Otherwise, let's read till the next line! - (else (loop)))) - (loop)) - -(define default-line-handler (make-handle-line)) - -(define* (queue-and-start-irc-agenda! agenda socket #:key - (username "syncbot") - (inet-port default-irc-port) - (line-handler default-line-handler) - (channels '())) - (dynamic-wind - (lambda () #f) - (lambda () - (enq! (agenda-queue agenda) - (wrap (irc-loop socket line-handler username))) - (enq! (agenda-queue agenda) (wrap (handle-login socket username - #:channels channels))) - (start-agenda agenda)) - (lambda () - (display "Cleaning up...\n") - (close socket)))) - - - -;;; CLI -;;; === - -(define option-spec - `((server (single-char #\s) (required? #t) (value #t)) - (port (single-char #\p) - (value #t) - (predicate - ,(lambda (s) - (if (string->number s) #t #f)))) - (username (single-char #\u) (required? #t) (value #t)) - (channels (value #t)) - (listen))) - -(define* (make-irc-bot-cli #:optional - (line-handler default-line-handler) - (print-and-continue-on-error #t)) - (define (main args) - (let* ((options (getopt-long args option-spec)) - (hostname (option-ref options 'server #f)) - (port (or (option-ref options 'port #f) - default-irc-port)) - (username (option-ref options 'username #f)) - (listen (option-ref options 'listen #f)) - (channels (option-ref options 'channels "")) - (agenda (if print-and-continue-on-error - (make-agenda #:pre-unwind-handler print-error-and-continue) - (make-agenda)))) - (display `((server ,hostname) (port ,port) (username ,username) - (listen ,listen) (channels-split ,(string-split channels #\space)))) - (newline) - (if listen - (spawn-and-queue-repl-server! agenda)) - (queue-and-start-irc-agenda! - agenda - (irc-socket-setup hostname port) - #:inet-port port - #:username username - #:channels (string-split channels #\space) - #:line-handler line-handler))) - main) - -(define main (make-irc-bot-cli))