diff options
Diffstat (limited to 'elisp/erbot/erbmsg.el')
-rw-r--r-- | elisp/erbot/erbmsg.el | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/elisp/erbot/erbmsg.el b/elisp/erbot/erbmsg.el new file mode 100644 index 0000000..4a0f748 --- /dev/null +++ b/elisp/erbot/erbmsg.el @@ -0,0 +1,583 @@ +;;; erbmsg.el --- memoserv-esque functions for Erbot +;; $Id: erbmsg.el,v 1.26 2007/11/23 16:31:59 deego Exp $ +;; Copyright (C) 2004 Sebastian Freundt +;; Emacs Lisp Archive entry +;; Filename: erbmsg.el +;; Package: erbmsg +;; Authors: Sebastian Freundt <freundt@math.tu-berlin.de> +;; Keywords: memo, message, +;; Version: still conceptional +;; URL: http://www.emacswiki.org/cgi-bin/wiki.pl?ErBot +;; URL: http://www.emacswiki.org/cgi-bin/wiki.pl?ErbMsg +;; For latest version: + +(defconst erbot-home-page + "http://savannah.nongnu.org/projects/erbot") +(defconst erbmsg-version + "Version 0.2 $Revision: 1.26 $") + + +;; This file is NOT (yet) part of GNU Emacs. + +;; This 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, or (at your option) +;; any later version. + +;; This 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 GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + + +;; See also: + +;;; Comments: + +;; - To automagically save the whole message table with each incoming message +;; put following to your .erbot: +;; +;; (add-hook 'erbmsg-new-msg-post-hook 'erbmsg-regular-dump) +;; +;; - To clean up message cookies with every flushed message, add +;; +;; (add-hook 'erbmsg-flush-post-hook 'erbmsg-garbage-cleanse-cookies) + + +;;; TODOs that have been done: + +;; 2004/06/22: +;; - added dump routines to dump message hash tables to hard disk +;; - added routines for restoring from dumped message files +;; - added interval within erbot does not notify on channel joins +;; - added erbmsg-new-msg-(pre|post)-hook +;; 2004/04/09: +;; - added support for multiple recipients (see fs-memo for syntax) +;; - abstracted fs-memo stuff to two defuns (erbmsg-memo-parse-msg and erbmsg-memorize-msg) +;; 2004/04/01: +;; - added hooks +;; 2004/03/31: +;; - store which channel the memo came from +;; - added garbage collection function (erbmsg-garbage-cleanse-cookies) to +;; clean up erbmsg-msg-cookie-hash-table from unreferenced cookies + +;;; TODO: +;; - functionality to forget the erbmsg-question-* pile effectively +;; - save erbmsg-msg-hash-table across sessions +;; - expire cookies in erbmsg-msg-cookie-hash-table some time (after 3 notifications?) + +;;; Data + + +(defvar erbmsg-msg-hash-table (make-hash-table :test 'equal) + "This is a hash-table holding all the messages via cookies.") + +(defvar erbmsg-internal-msg-cookie nil + "Message cookie for internal communication.") + +(defvar erbmsg-msg-cookie-hash-table (make-hash-table :test 'equal) + "This is the hash-table for message cookies, the actual +messages are saved here") + +(defgroup erbmsg nil + "The erbmsg module for erbot" + :group 'erbot) + +(defcustom erbmsg-default-magic-words nil + "List of default magic words for messages with magic words." + :group 'erbmsg) + + +;;; dump settings + +(defcustom erbmsg-dump-file "~/public_html/data/messages.dump" + "File to dump message hash tables to." + :group 'erbmsg) + +(defcustom erbmsg-auto-restore-message-tables t + "Whether to automagically restore contents of `erbmsg-dump-file'." + :group 'erbmsg + :type 'boolean) + +(defcustom erbmsg-auto-dump-message-tables nil + "Whether to automagically dump hash tables to `erbmsg-dump-file'." + :group 'erbmsg + :type 'boolean) + + + +;;; uncomment this to normalize to UTC +;;(set-time-zone-rule "UTC0") + +(defvar erbmsg-after-load-hook nil + "Hook called after `erbmsg' has been loaded.") + +(defvar erbmsg-new-msg-pre-hook nil + "Hook called before a new message has been posted. +The raw message is passed as argument.") +(defvar erbmsg-new-msg-post-hook + (when erbmsg-auto-dump-message-tables + '(erbmsg-regular-dump)) + "Hook called after a new message has been posted. +The parsed message \(split to nicks and actual message text\) +is passed as argument.") +(defvar erbmsg-flush-pre-hook nil + "Hook called before erbmsg-flush-pending-msgs is called.") +(defvar erbmsg-flush-post-hook nil + "Hook called before erbmsg-flush-pending-msgs is called.") + + +;;; this is too useful to not add it here +(add-hook 'erbmsg-flush-post-hook 'erbmsg-garbage-cleanse-cookies) + + +;; interface functions +(defun fs-memo (&rest msg) + "Specify your message and the nick to dedicate to here, as in: + +#somechan> ,memo somenick hello somenick, don't forget + +Allowed syntaxes: +,memo [to|for] <nick> msg +,memo [to|for] <nick> <nick> <nick>: msg + +Note: magic words are not currently implemented." + (or (and erbot-erbmsg-p + msg + (let* ((msg-raw (erbutils-stringize msg)) + (nicks+msg (erbmsg-memo-parse-msg msg-raw))) + (run-hook-with-args 'erbmsg-new-msg-pre-hook msg-raw) + (mapc (lambda (nick+msg) + (let* ((nick (car nick+msg)) + (msg (nth 1 nick+msg))) + (erbmsg-memorize-msg nick msg))) + nicks+msg) + (run-hook-with-args 'erbmsg-new-msg-post-hook nicks+msg) + "msg memorized for delivery")) + (if msg (format "error: %S NOT parsed") (fs-memos)) + 'noreply)) +(defalias 'fs-msg-wmw 'fs-memo) ;; just for compatibility +(defalias 'fs-msg-with-magic-words 'fs-memo) + + +(defun erbmsg-memo-parse-msg (raw-msg) + "Parses MSG for any of the allowed memo syntaxes and returns a list +\(\(nick msg) (nick msg) ...)" + (let* ((nick-msg (cond ((string-match "^\\(?:to\\|for\\)?\\b\\(.+\\)\\b:\\(.*\\)" raw-msg) + (cons (match-string 1 raw-msg) (match-string 2 raw-msg))) + ((string-match "^\\(?:to\\|for\\)?\\(?:\\s-\\|\\b\\)\\(\\S-+\\)\\s-\\(.*\\)" raw-msg) + (cons (match-string 1 raw-msg) (match-string 2 raw-msg))) + (t nil))) + (nicks (split-string (replace-regexp-in-string ",\\|\\band\\b" "" (car nick-msg)))) + (msg (replace-regexp-in-string "^\\s-+" "" (cdr nick-msg)))) + (mapcar (lambda (nick) + (list nick msg)) + nicks))) +;;(erbmsg-memo-parse-msg "hroptatyr and deego: huhu! :)") + +(defun erbmsg-memorize-msg (nick msg &optional magic-words) + "Memorizes NICKs MSG." + (let* ((nicks-ht (or (gethash nick erbmsg-msg-hash-table) + (puthash nick + (make-hash-table :test 'equal) + erbmsg-msg-hash-table))) + (cnick fs-nick) + (cchan fs-tgt) + (ctime (current-time)) + ;; composition of the new memo + (newmsg (vector cnick cchan msg ctime magic-words)) + (newcookie (erbmsg-generate-msg-cookie newmsg)) + ;; now memos from that user already in the system + (cmsgs (gethash cnick nicks-ht))) + (add-to-list 'cmsgs newcookie) + (puthash cnick cmsgs nicks-ht))) + + + +(defun fs-memos (&rest line) + "This is redundant but more clean than in `erbmsg-parse'." + (and erbot-erbmsg-p + (let* ((linecar (car line)) + (internalp (and erbmsg-internal-msg-cookie + (eq linecar ':internal) + (eq erbmsg-internal-msg-cookie (cadr line)))) + (nick (or (and internalp + (car (cdr-safe (cdr-safe line)))) + fs-nick)) + (fromnicks (and (null internalp) + (mapcar (lambda (s) (format "%s" s)) line))) + (nicks-ht (gethash nick erbmsg-msg-hash-table)) + pending-msgs) + (and nicks-ht + (maphash (lambda (fromnick msg-cookies) + (setq pending-msgs + (append pending-msgs (or (and (null fromnicks) + msg-cookies) + (and (member fromnick fromnicks) + msg-cookies))))) + nicks-ht)) + (or (and pending-msgs + (let ((msg-cookie)) + (format "erm, %s, %s msgs pending, see them? %s" + nick + (length pending-msgs) + (erbmsg-question `((notice (erbmsg-notice-pending-msgs ,nick ',pending-msgs)) + (query (erbmsg-query-pending-msgs ,nick ',pending-msgs)) + (post (erbmsg-post-pending-msgs ,nick ',pending-msgs)) + (flush (erbmsg-flush-pending-msgs ,nick ',pending-msgs)) + (no (ignore)) + (memo-help (erbmsg-help))) + nick)))) + (and (null internalp) + (format ":( no msgs for you, %s\n%s" nick + (fs-describe "help-memo"))))))) +(defalias 'fs-msg-mymsgs 'fs-memos) +(defalias 'fs-mymemos 'fs-memos) +(defalias 'fs-msgs 'fs-msg-mymsgs) +(defalias 'fs-mymsgs 'fs-msg-mymsgs) + +(defun fsi-erbmsg-version (&rest ignore) + "Spits out `erbmsg-version'." + erbmsg-version) +(defalias 'fs-msg-version 'fs-erbmsg-version) + + +(defcustom erbmsg-notify-on-join-timeout 2 + "Interval in seconds to wait between notification on channel joins." + :group 'erbmsg) + +(defvar erbmsg-last-nicks-join nil + "List of nicks with last join time.") + +(defun erbmsg-put-alist (item value alist) + "Modify ALIST to set VALUE to ITEM. +If there is a pair whose car is ITEM, replace its cdr by VALUE. +If there is not such pair, create new pair (ITEM . VALUE) and +return new alist whose car is the new pair and cdr is ALIST. +\[tomo's ELIS like function]" + (let ((pair (assoc item alist))) + (if pair + (progn + (setcdr pair value) + alist) + (cons (cons item value) alist) + ))) + +(defun erbmsg-set-alist (symbol item value) + "Modify a alist indicated by SYMBOL to set VALUE to ITEM." + (or (boundp symbol) + (set symbol nil)) + (set symbol (erbmsg-put-alist item value (symbol-value symbol)))) + +(defun erbmsg-notify-msg-on-JOIN (process parsed) + "Notifies users about left messages +when joining the channel" + (and erbot-erbmsg-p + (let* ((usernickhost (if erbot-on-new-erc-p + (erc-response.sender parsed) + (aref parsed 1))) + (channel (if erbot-on-new-erc-p + (nth 0 (erc-response.command-args parsed)) + (aref parsed 2))) + (nick (car (erc-parse-user usernickhost))) + (last-access (cdr-safe (assoc nick erbmsg-last-nicks-join)))) + (erbmsg-set-alist 'erbmsg-last-nicks-join nick (current-time)) + (setq erbmsg-internal-msg-cookie (random)) + (let* ((msgs (fs-msg-mymsgs :internal erbmsg-internal-msg-cookie nick))) + (and msgs + (or (null last-access) + (> (- (nth 1 (current-time)) (nth 1 last-access)) + erbmsg-notify-on-join-timeout)) + (erc-message "PRIVMSG" + (format "%s %s" + channel + msgs))) + 'noreply)))) +(if (and (boundp 'erbot-on-new-erc-p) erbot-on-new-erc-p) + (add-hook 'erc-server-JOIN-functions 'erbmsg-notify-msg-on-JOIN) + (add-hook 'erc-server-JOIN-hook 'erbmsg-notify-msg-on-JOIN)) + + + +(defun erbmsg-parse (msg proc nick tgt localp userinfo) + "When having (require 'erbmsg) and (setq erbot-erbmsg-p t) +this function is called with every message typed. + +It checks for `nick' being in `erbmsg-msg-hash-table', +if so, i.e. `nick' is about to have messages pending for delivery, +it will be checked here if `nick' says the ~magic words~, +if that's also the case, the message will be spit out. + +Currently this function also plays the role as question handler, +see erbmsg-question part below :)." + (let* ((nicks-ht (gethash nick erbmsg-msg-hash-table)) + (pending-msgs) + + ;; now the stuff for question handling + (nicks-q-ht (gethash nick erbmsg-question-hash-table)) + (pending-actions)) + + ;; erbmsg-question handling + (and nicks-q-ht + (maphash (lambda (keyword action-forms) + (and (string-match (format "\\b%S\\b" keyword) + msg) + (let ((func (intern (format "fs-%s" keyword)))) + (and (fboundp func) + (funcall func))))) + nicks-q-ht)))) + + +(defun erbmsg-generate-msg-cookie (message) + "Generates a message cookie for `message' and returns it." + (let* ((msg-cookie (format "%.4x%.4x" + (mod (random) 65536) (mod (random) 65536)))) + (puthash msg-cookie message erbmsg-msg-cookie-hash-table) + msg-cookie)) + +(defun erbmsg-get-msgs (msg-cookies) + "Gets messages by `msg-cookie'." + (mapcar (lambda (msg-cookie) + (gethash msg-cookie erbmsg-msg-cookie-hash-table)) + msg-cookies)) + + +;; reply functions +(defun erbmsg-notice-pending-msgs (nick msg-cookies) + "NOTICEs all `msgs' to the user `nick'." + (erbmsg-send-pending-msgs nick msg-cookies "NOTICE" nick)) + +(defun erbmsg-query-pending-msgs (nick msg-cookies) + "PRIVMSGs all `msgs' to the user `nick'." + (erbmsg-send-pending-msgs nick msg-cookies "PRIVMSG" nick)) + +(defun erbmsg-post-pending-msgs (nick msg-cookies) + "Publically post all `msgs' to current channel" + (erbmsg-send-pending-msgs nick msg-cookies "PRIVMSG" fs-tgt)) + +(defun erbmsg-send-pending-msgs (nick msg-cookies &optional method target) + "PRIVMSGs all `msgs' to the user `nick', +instead of PRIVMSG you may specify another sending method." + (let ((msgs (erbmsg-get-msgs msg-cookies)) + (method (or method "PRIVMSG")) + (target (or target fs-nick))) + (and msgs + (mapc (lambda (msg) + (or (and msg + (let ((msgfrom (aref msg 0)) + (msgchan (aref msg 1)) + (msgtext (aref msg 2)) + (msgtime (aref msg 3))) + (erc-message method + (format "%s %s@%s %s: %s" + target + msgfrom + msgchan + (format-time-string "%D %T (%Z)" msgtime) + msgtext)))) + (erc-message method (format "%s invalid message cookie" target)))) + msgs)))) + +(defun erbmsg-flush-pending-msgs (nick msg-cookies) + "Flushes all pending messages for user `nick'." + (run-hook-with-args 'erbmsg-flush-pre-hook nick msg-cookies) + (erbmsg-flush-msg-cookies msg-cookies) + (remhash nick erbmsg-msg-hash-table) + (remhash nick erbmsg-question-hash-table) + (erc-send-message "flushed") + (run-hook-with-args 'erbmsg-flush-post-hook nick msg-cookies)) + +(defun erbmsg-flush-msg-cookie (msg-cookie) + "Flushes `msg-cookie'." + (remhash msg-cookie erbmsg-msg-cookie-hash-table)) +(defun erbmsg-flush-msg-cookies (msg-cookies) + "Flushes a collection of `msg-cookies'." + (mapc 'erbmsg-flush-msg-cookie msg-cookies)) + + +(defun erbmsg-help (&rest ignore) + "Spits out some detour to the wiki help page." + (erc-send-message "help? whom to help? see http://www.emacswiki.org/cgi-bin/wiki/ErbMsg")) + + + + + +;; garbage collection + +(defun erbmsg-garbage-cleanse-cookies (&rest ignore) + "Collects garbage from `erbmsg-msg-cookie-hash-table' when +there's no referring entry in `erbmsg-msg-hash-table'." + (maphash (lambda (cookie-k cookie-v) + (let ((cookie cookie-k) + (referred)) + (catch 'ref-exists-p + (maphash (lambda (memo-k memo-v) + (maphash (lambda (from cookie-list) + (and (member cookie cookie-list) + (setq referred t) + (throw 'ref-exists-p t))) + memo-v)) + erbmsg-msg-hash-table)) + (unless referred + (remhash cookie erbmsg-msg-cookie-hash-table)))) + erbmsg-msg-cookie-hash-table)) +;; erbmsg-msg-cookie-hash-table +;; (erbmsg-garbage-cleanse-cookies) + + + + + +;;; just some tricks to create gazillions of msgs w/o IRC +;; (clrhash erbmsg-msg-hash-table) +;; (puthash "hroptatyr" (make-hash-table :test 'equal) erbmsg-msg-hash-table) +;; (puthash "asathor" '("22224444" "33336666") (gethash "hroptatyr" erbmsg-msg-hash-table)) + + + + + + + +;;; this will get more abstract and move to an own modules soon :) +(defvar erbmsg-question-hash-table (make-hash-table :test 'equal) + "Hash table to hold who may be about to have the choice.") + +(defvar erbmsg-question-verbosity nil + "Controls how talkative erbot is when being in question mode.") + +(defvar erbmsg-question-handler nil + "command temporarily bound to certain users.") + +(defun erbmsg-question (choices nick) + "Declares choices for interactively control erbot's +more complex tasks. + +`choices' is an alist (action action-forms), +`action-forms' will be eval'd if nick uses the magic word once again." + (let* ((nicks-ht (puthash nick (make-hash-table :test 'equal) erbmsg-question-hash-table))) + (mapc (lambda (choice) + (let* ((magic-word (car choice)) + (action-forms (cdr choice)) + (internal-name (intern (format "fs-%s" magic-word)))) + (and ;;(not (fboundp internal-name)) + (fset internal-name + `(lambda (&rest ignore) + (and (erbmsg-question-user-allowed-p fs-nick ',magic-word) + (mapc 'eval ',action-forms)) + (erbmsg-question-user-answer fs-nick ',magic-word)))) + (puthash magic-word action-forms nicks-ht))) + choices) + (format "[type %s]" + (mapconcat (lambda (choice) + (format "%s%s" + erbn-char + (car choice))) + choices "/")))) + +;;(symbol-function 'fs-flush) + + +(defun erbmsg-question-user-allowed-p (nick erbot-command) + "Tests whether the user `nick' is allowed to use `erbot-command', +i.e. if the user has been offered such an action." + (let* ((nicks-ht (gethash nick erbmsg-question-hash-table)) + (command-p (and nicks-ht + (gethash erbot-command nicks-ht)))) + (null (null command-p)))) +(defun erbmsg-question-user-answer (nick erbot-command &optional answer) + "Tests whether the user `nick' is allowed to use `erbot-command', +if so return 'noreply, if not return an according answer." + (or (and (not (erbmsg-question-user-allowed-p nick erbot-command)) + 'noreply) ;;; "You are currently not allowed to use this function. :(") + 'noreply)) + + +;;; dumping code +;; this code is to make erbot remember messages after restarts + +(defun erbmsg-regular-dump (&rest ignore) + "Fun wrapper to call `erbmsg-dump-tables'." + (interactive) + (erbmsg-dump-tables)) + +(defun erbmsg-dump-tables (&optional file) + "Dumps known message hash tables to a buffer in order to save it." + (interactive "Ferbmsg dump file: ") + (let ((file (or file + erbmsg-dump-file))) + (with-temp-buffer + (mapc (lambda (htable) + (insert (format "[%s \n [\n" htable)) + (maphash + (lambda (key val) + (insert + (format " [%S %s]\n" key + (cond + ((hash-table-p val) + (let (valstring) + (maphash + (lambda (k2 v2) + (setq valstring + (format "%s[%S %S]" + (or valstring "") k2 v2))) + val) + (format "(%s)" valstring))) + (t (format "%S" val)))))) + (eval htable)) + (insert (format " ]\n]\n"))) + '(erbmsg-msg-hash-table erbmsg-msg-cookie-hash-table)) + (write-file erbmsg-dump-file)))) + +(defun erbmsg-restore-tables (&optional file) + "Restores known message hash tables from FILE or `erbmsg-dump-file'." + (interactive "ferbmsg dump file: ") + (let* ((file (or file + erbmsg-dump-file)) + (file-vector + (and (file-readable-p file) + (with-temp-buffer + (insert-file-contents file) + (eval (erbn-read (format "(setq file-vector '(%s))" + (erbutils-buffer-string)))))))) + (mapvector + (lambda (tablevector) + (let ((table (aref tablevector 0)) + (vector (aref tablevector 1))) + (mapvector + (lambda (keyval) + (let ((key (aref keyval 0)) + (val (aref keyval 1))) + (cond ((listp val) + (let ((nickht (make-hash-table :test 'equal))) + (mapc + (lambda (htvec) + (let ((k2 (aref htvec 0)) + (v2 (aref htvec 1))) + (puthash k2 v2 nickht))) + val) + (puthash key nickht (eval table)))) + (t (puthash key val (eval table)))))) + vector))) + file-vector))) + +(when (and erbot-erbmsg-p + erbmsg-auto-restore-message-tables + ;;(eq (hash-table-count erbmsg-msg-hash-table) 0) + ) + (erbmsg-restore-tables)) + + +(provide 'erbmsg) + +;;; erbmsg.el ends here + +;; Local variables: +;; indent-tab-mode: nil +;; End: |