diff -uNr a/logbot/INSTALL b/logbot/INSTALL --- a/logbot/INSTALL false +++ b/logbot/INSTALL ae93b514e6a6ba3861c1099bb3fa536525f47f19e7cdc0b411a0eaa6159b1d75a522ca0611be5559ec63b1bfdc90e9c2a4d6872d741df0d768cd48be7ecdeabf @@ -0,0 +1,26 @@ +INSTALL + + * Install `ircbot`. + + * From the SBCL REPL: + (ql:quickload :cl-irc) + (ql:quickload :cl-postgres) + (ql:quickload :postmodern) + + * Use V to press `logbot` + +mkdir -p ~/src/logbot +cd ~/src/logbot + +mkdir .wot +cd .wot && wget http://trinque.org/trinque.asc && cd .. + +v.pl init http://trinque.org/src/logbot +v.pl press logbot-genesis logbot-genesis.vpatch + +ln -s ~/src/logbot/logbot-genesis ~/quicklisp/local-projects/logbot + + * Create a PostgreSQL database with UTF-8 encoding, then load logbot.sql + into that database: + +psql -f logbot.sql mydb diff -uNr a/logbot/README b/logbot/README --- a/logbot/README false +++ b/logbot/README 9d7ca23668df545b7ed07857cc8965fb65ab980fd540cebc2cdd96c2f556d5fb65eb149233dc777af7be1ba8243ca607bd6a68d8ea991d4a2666be617447682d @@ -0,0 +1,7 @@ +README + +`logbot` extends `ircbot` to provide an interface to a single IRC +channel from PostgreSQL, writing all messages to a `log` table and +reading messages to be sent from `outbox` in a specified database. By +listening to pg_notify channels, services connected to PostgreSQL can +react when new log lines are inserted. diff -uNr a/logbot/USAGE b/logbot/USAGE --- a/logbot/USAGE false +++ b/logbot/USAGE eb195d717f53ac62d3ba4cabf2960b87bd30184a128f42dcc856f1757fffa2ef4e74d5c8976dfc0f76809286609749b150ff90b47d325549f7adf9971f05ff00 @@ -0,0 +1,14 @@ +USAGE + +(asdf:load-system :logbot) +(defvar *bot*) +(setf *bot* + (logbot:make-logbot + "chat.freenode.net" 6667 "nick" "password" "#channel" + '("db-name" "db-user" "db-password" "db-host"))) + +; connect in separate thread, returning thread +(logbot:ircbot-connect-thread *bot*) + +; or connect using the current thread +; (logbot:ircbot-connect *bot*) diff -uNr a/logbot/logbot.asd b/logbot/logbot.asd --- a/logbot/logbot.asd false +++ b/logbot/logbot.asd 17258c61cc8878a2ac72fcca188892e35b1ff69a069b93b4fb633da9e39447da66dc50d6ad2af53259700f78b187970e9bb75c0baca03318c65246a29a2d5fe5 @@ -0,0 +1,13 @@ +;;;; logbot.asd + +(asdf:defsystem #:logbot + :description "logbot" + :author "Michael Trinque " + :license "http://trilema.com/2015/a-new-software-licensing-paradigm/" + :depends-on (#:cl-irc + #:cl-postgres + #:ircbot + #:postmodern) + :components ((:file "package") + (:file "logbot"))) + Binary files a/logbot/logbot.fasl and b/logbot/logbot.fasl differ diff -uNr a/logbot/logbot.lisp b/logbot/logbot.lisp --- a/logbot/logbot.lisp false +++ b/logbot/logbot.lisp 34c09aae0020f81b9dd9a9120dae84c0e996b578041e837785e814409f69d0d587a21c0c1454c6e39039c8ca3fd135092d4efd006f39f0e47ac87f0314f6f92d @@ -0,0 +1,93 @@ +(in-package #:logbot) + + +(defun get-and-purge-outbox-messages (db) + (postmodern:with-connection db + (postmodern:query + "with deleted as ( + delete from outbox + returning target, message, queued_at + ) + select target, + message + from deleted + order by queued_at" + :rows))) + +(defun make-log-entry (db target message host source user) + (postmodern:with-connection db + (postmodern:execute + "insert into log (target, message, host, source, \"user\") + values ($1, $2, $3, $4, $5)" + target + message + (if (string= "" host) :null host) + source + (if (null user) :null user)))) + + +(defclass logbot (ircbot) + ((pg-thread :accessor logbot-pg-thread :initform nil) + (db :reader logbot-db :initarg :db))) + +(defmethod ircbot-connect :after ((bot logbot)) + (let ((conn (ircbot-connection bot))) + (add-hook conn 'irc-mode-message (lambda (message) + (logbot-check-mode bot message))) + (add-hook conn 'irc-privmsg-message (lambda (message) + (destructuring-bind (target message-text) (arguments message) + (make-log-entry (logbot-db bot) + target + message-text + (host message) + (source message) + (user message))))))) + +(defmethod ircbot-send-message :after ((bot logbot) target message-text) + (let* ((b-connection (ircbot-connection bot)) + (b-user (user b-connection))) + (make-log-entry (logbot-db bot) + target + message-text + (hostname b-user) + (nickname b-user) + (username b-user)))) + +(defmethod logbot-check-mode ((bot logbot) message) + (if (= 3 (length (arguments message))) + (destructuring-bind (channel mode nick) (arguments message) + (when (and (string= (host message) "services.") + (string= channel (ircbot-channel bot)) + (or (string= mode "+o") (string= mode "+v")) + (string= nick (ircbot-nick bot))) + + (when (null (logbot-pg-thread bot)) + (logbot-start-pg-thread bot) + (logbot-send-outbox bot)))))) + +(defmethod logbot-send-outbox ((bot logbot)) + (loop + for (target message) + in (get-and-purge-outbox-messages (logbot-db bot)) + do (ircbot-send-message bot target message))) + +(defmethod logbot-start-pg-thread ((bot logbot)) + (setf (logbot-pg-thread bot) + (sb-thread:make-thread + (lambda () + (postmodern:with-connection (logbot-db bot) + (postmodern:execute "listen outbox_new_message") + (loop + (if (string= (cl-postgres:wait-for-notification postmodern:*database*) + "outbox_new_message") + (logbot-send-outbox bot))))) + :name "logbot-pg"))) + +(defun make-logbot (server port nick password channel db) + (make-instance 'logbot + :server server + :port port + :nick nick + :password password + :channel channel + :db db)) diff -uNr a/logbot/logbot.sql b/logbot/logbot.sql --- a/logbot/logbot.sql false +++ b/logbot/logbot.sql dbc265a63c34c6b682694c8776aa4640c83ba92dca5400f0bd393f6a9eb5d3672011c234c3d04a7ad074f3fce560d71d78201f4bbcb506c12ed6cc90d3c59017 @@ -0,0 +1,48 @@ +set search_path = public; + +create extension if not exists plpgsql; +create extension if not exists pgcrypto; +create extension if not exists "uuid-ossp"; + +create table log ( + id uuid primary key default gen_random_uuid(), + target text not null, + message text not null, + host text, + source text not null, + "user" text, + received_at timestamp without time zone not null default (now() at time zone 'utc') +); + +create index log_received_at on log (received_at); +create index log_target on log (target); +create index log_source on log (source); + +create or replace function log_insert_notify () returns trigger as $$ + begin + perform pg_notify('log_new_message', NEW.id::text); + return NEW; + end; +$$ language plpgsql; + +create trigger log_insert_notify_trigger +after insert on log +for each row execute procedure log_insert_notify (); + +create table outbox ( + id serial primary key, + target text not null, + message text not null, + queued_at timestamp without time zone not null default (now() at time zone 'utc') +); + +create or replace function outbox_insert_notify () returns trigger as $$ + begin + perform pg_notify('outbox_new_message', NEW.id::text); + return NEW; + end; +$$ language plpgsql; + +create trigger outbox_insert_notify_trigger +after insert on outbox +for each row execute procedure outbox_insert_notify (); diff -uNr a/logbot/package.lisp b/logbot/package.lisp --- a/logbot/package.lisp false +++ b/logbot/package.lisp 29a3142e5a5a5b814921b49c16571b2d0359cdca2dfdc9ca4463734f066b1e087a4ececb6341f1667ef26f6a97b0b8ce326cedaf2539d2cfd64ca4c3fb6551df @@ -0,0 +1,20 @@ +;;;; package.lisp + +(defpackage :logbot + (:use :cl + :cl-irc + :ircbot) + (:export :make-logbot + :logbot + :ircbot-connect + :ircbot-connect-thread + :ircbot-disconnect + :ircbot-reconnect + :ircbot-connection + :ircbot-channel + :ircbot-send-message + :ircbot-server + :ircbot-port + :ircbot-nick + :ircbot-lag)) +