first push
This commit is contained in:
commit
52133d25e2
|
@ -0,0 +1,9 @@
|
|||
*.fasl
|
||||
*.dx32fsl
|
||||
*.dx64fsl
|
||||
*.lx32fsl
|
||||
*.lx64fsl
|
||||
*.x86f
|
||||
*~
|
||||
.#*
|
||||
bin/
|
|
@ -0,0 +1,74 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at dev@computerfox.xyz. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
@ -0,0 +1,23 @@
|
|||
LISPS = ros sbcl clisp cmucl ccl
|
||||
CMDS = --eval "(ql:quickload :ida-bot)" --eval "(asdf:make :ida-bot)" --eval "(quit)"
|
||||
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
LISP := $(foreach lisp,$(LISPS), \
|
||||
$(shell where $(lisp)) \
|
||||
$(if $(.SHELLSTATUS),$(strip $(lisp)),))
|
||||
else
|
||||
LISP := $(foreach lisp,$(LISPS), \
|
||||
$(if $(findstring $(lisp),"$(shell which $(lisp) 2>/dev/null)"), $(strip $(lisp)),))
|
||||
endif
|
||||
|
||||
ifeq ($(LISP),)
|
||||
$(error "No lisps found")
|
||||
endif
|
||||
|
||||
all:
|
||||
$(LISP) $(CMDS)
|
||||
cp -r commands bin/
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
|
@ -0,0 +1,31 @@
|
|||
# owncast-bot
|
||||
|
||||
a quick and dirty chat bot for owncast
|
||||
|
||||
## Usage
|
||||
|
||||
`$ ./ida-bot --help` to print command usage
|
||||
|
||||
`$ ./ida-bot -c your.config -p 8080` to run the bot with the specified config and on port 8080
|
||||
|
||||
|
||||
the bot will load all lisp code in the `./commands` folder if it exists. while this is pretty unsafe it allows you (developers) to quickly create commands using the full power of lisp!
|
||||
|
||||
|
||||
the bot comes with a few pre-built commands (check commands folder). feel free to add your own, and contribute some back to the repo if you feel like it :)
|
||||
|
||||
right now only chat-initiated commands are supported, but thats planning on changing soon.
|
||||
|
||||
## Installation
|
||||
|
||||
download a binary release from releases
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
* a. fox
|
||||
|
||||
## License
|
||||
|
||||
BSD 3-Clause
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
(in-package :ida-bot.extension)
|
||||
|
||||
(unless (uiop:file-exists-p "stab.count")
|
||||
(str:to-file "stab.count" "0"))
|
||||
|
||||
(defvar *stab-count* (parse-integer (str:from-file "stab.count")))
|
||||
|
||||
(define-command ("stab")
|
||||
(incf *stab-count*)
|
||||
(str:to-file "stab.count" (format nil "~A" *stab-count*))
|
||||
(send-chat (format nil "Ceasar has been stabbed ~A times" *stab-count*)))
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
(in-package :ida-bot.extension)
|
||||
|
||||
(define-command ("motd")
|
||||
(send-chat (env :motd)))
|
|
@ -0,0 +1,5 @@
|
|||
access-token = a cool token
|
||||
stream-url = https://my.stream.live
|
||||
|
||||
motd = Thanks for joining the stream, hope you're having a good time! I try to stream every wednesday, so if youre interested feel free to join!
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
(defsystem "ida-bot-test"
|
||||
:defsystem-depends-on ("prove-asdf")
|
||||
:author "a. fox"
|
||||
:license ""
|
||||
:depends-on ("ida-bot"
|
||||
"prove")
|
||||
:components ((:module "tests"
|
||||
:components
|
||||
((:test-file "ida-bot"))))
|
||||
:description "Test system for ida-bot"
|
||||
:perform (test-op (op c) (symbol-call :prove-asdf :run-test-system c)))
|
|
@ -0,0 +1,40 @@
|
|||
(defsystem "ida-bot"
|
||||
:version "0.1.0"
|
||||
:author "a. fox"
|
||||
:license ""
|
||||
:depends-on ("bordeaux-threads"
|
||||
"clack"
|
||||
"lack"
|
||||
"caveman2"
|
||||
"envy"
|
||||
"cl-ppcre"
|
||||
"uiop"
|
||||
"str"
|
||||
#+(and Unix SBCL) "woo"
|
||||
"com.inuoe.jzon"
|
||||
"alexandria"
|
||||
|
||||
;; for @route annotation
|
||||
"cl-syntax-annot"
|
||||
"with-user-abort"
|
||||
|
||||
"simple-config"
|
||||
"unix-opts"
|
||||
"drakma")
|
||||
:components ((:module "src"
|
||||
:components
|
||||
((:file "main" :depends-on ("web" "config" "commands"))
|
||||
(:file "web" :depends-on ("util" "commands"))
|
||||
(:file "commands" :depends-on ("util" "owncast"))
|
||||
(:file "owncast" :depends-on ("util" "config"))
|
||||
(:file "util")
|
||||
(:file "config"))))
|
||||
:description ""
|
||||
:in-order-to ((test-op (test-op "ida-bot-test")))
|
||||
:build-operation "program-op"
|
||||
:build-pathname "bin/ida-bot"
|
||||
:entry-point "ida-bot::make-start")
|
||||
|
||||
#+sb-core-compression
|
||||
(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
|
||||
(uiop:dump-image (asdf:output-file o c) :executable t :compression t))
|
|
@ -0,0 +1,69 @@
|
|||
(defpackage ida-bot.commands
|
||||
(:use :cl :ida-bot.util)
|
||||
(:export :define-command
|
||||
:process-commands))
|
||||
|
||||
(in-package :ida-bot.commands)
|
||||
|
||||
;; here is where i need to write the command loader
|
||||
;; this should ideally load a directory with all
|
||||
;;
|
||||
|
||||
(defvar *commands* nil
|
||||
"list of commands")
|
||||
|
||||
(defclass bot-command ()
|
||||
((priority :accessor command-priority
|
||||
:initarg :priority)
|
||||
(type :reader command-type
|
||||
:initarg :type)
|
||||
(command :reader command-string
|
||||
:initarg :command)
|
||||
(function :accessor command-function
|
||||
:initarg :function)))
|
||||
|
||||
(defmacro define-command ((command &key priority (type :chat)) &body body)
|
||||
"create a command COMMAND"
|
||||
(let ((pri (or priority (1+ (length *commands*))))
|
||||
(cmd (str:concat "!" command)))
|
||||
|
||||
;; ensure each command has unique commands
|
||||
(unless (some #'(lambda (x)
|
||||
(string= command (command-string x)))
|
||||
*commands*)
|
||||
`(prog1
|
||||
(push (make-instance 'bot-command :command ,cmd
|
||||
:priority ,pri
|
||||
:type ,type
|
||||
:function
|
||||
(lambda (it)
|
||||
(let ((type (agetf it "type"))
|
||||
(data (agetf it "eventData")))
|
||||
(when (and (check-type-symbol ,type type)
|
||||
(str:starts-with-p ,cmd (agetf data "body")))
|
||||
,@body))))
|
||||
*commands*)
|
||||
(setf *commands* (sort *commands* #'< :key #'command-priority))))))
|
||||
|
||||
(defun process-commands (message)
|
||||
"process each command based on priority"
|
||||
(loop :for cmd :in *commands*
|
||||
:do (funcall (command-function cmd) message)))
|
||||
|
||||
(in-package :cl-user)
|
||||
(defpackage ida-bot.extension
|
||||
(:use :cl :ida-bot.util :ida-bot.commands
|
||||
:ida-bot.actions :ida-bot.config)
|
||||
(:export :load-commands))
|
||||
(in-package :ida-bot.extension)
|
||||
|
||||
(defun load-commands ()
|
||||
"loads all commands from subdirectory"
|
||||
(unless (uiop:directory-exists-p "./commands/")
|
||||
(format t "The command directory doesn't exist. Please create it and fill it with lisp commands"))
|
||||
|
||||
(loop :for file :in (uiop:directory-files "./commands/" "*.lisp")
|
||||
:do (format t "Loading file ~A~%" file)
|
||||
(load file)))
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
(in-package :cl-user)
|
||||
(defpackage ida-bot.config
|
||||
(:use :cl)
|
||||
(:import-from :envy
|
||||
:config-env-var
|
||||
:defconfig)
|
||||
(:export :env
|
||||
:*application-root*
|
||||
:*static-directory*
|
||||
:appenv
|
||||
:developmentp
|
||||
:productionp))
|
||||
(in-package :ida-bot.config)
|
||||
|
||||
(setf (config-env-var) "APP_ENV")
|
||||
|
||||
(defparameter *application-root* (asdf:system-source-directory :ida-bot))
|
||||
(defparameter *static-directory* (merge-pathnames #P"static/" *application-root*))
|
||||
|
||||
(defconfig :common
|
||||
`(:databases ((:maindb :sqlite3 :database-name ":memory:"))))
|
||||
|
||||
(defconfig |development|
|
||||
'())
|
||||
|
||||
(defconfig |production|
|
||||
'())
|
||||
|
||||
(defconfig |test|
|
||||
'())
|
||||
|
||||
(defun config (&optional key)
|
||||
(envy:config #.(package-name *package*) key))
|
||||
|
||||
(defun appenv ()
|
||||
(uiop:getenv (config-env-var #.(package-name *package*))))
|
||||
|
||||
(defun developmentp ()
|
||||
(string= (appenv) "development"))
|
||||
|
||||
(defun productionp ()
|
||||
(string= (appenv) "production"))
|
||||
|
||||
(defun env (key &optional default)
|
||||
(or (config key)
|
||||
(conf:config key default)))
|
|
@ -0,0 +1,129 @@
|
|||
(defpackage ida-bot.app
|
||||
(:use :cl)
|
||||
(:import-from :lack.builder
|
||||
:builder)
|
||||
(:import-from :ppcre
|
||||
:scan
|
||||
:regex-replace)
|
||||
(:import-from :ida-bot.web
|
||||
:*web*)
|
||||
(:import-from :ida-bot.config
|
||||
:env
|
||||
:productionp
|
||||
:*static-directory*)
|
||||
(:export :get-builder))
|
||||
(in-package :ida-bot.app)
|
||||
|
||||
(defun get-builder ()
|
||||
(builder
|
||||
nil
|
||||
(if (productionp)
|
||||
nil
|
||||
:accesslog)
|
||||
(if (env :error-log)
|
||||
`(:backtrace
|
||||
:output ,(env :error-log))
|
||||
nil)
|
||||
:session
|
||||
(if (productionp)
|
||||
nil
|
||||
nil)
|
||||
*web*))
|
||||
|
||||
;;
|
||||
;; main app package
|
||||
;;
|
||||
|
||||
(in-package :cl-user)
|
||||
(defpackage ida-bot
|
||||
(:use :cl :with-user-abort)
|
||||
(:import-from :ida-bot.config
|
||||
:env)
|
||||
(:import-from :clack
|
||||
:clackup)
|
||||
(:import-from :unix-opts
|
||||
:define-opts
|
||||
:get-opts)
|
||||
(:export :start
|
||||
:stop))
|
||||
(in-package :ida-bot)
|
||||
|
||||
(define-opts
|
||||
(:name :help
|
||||
:description "prints this help text"
|
||||
:short #\h
|
||||
:long "help")
|
||||
(:name :port
|
||||
:description "sets port"
|
||||
:short #\p
|
||||
:long "port"
|
||||
:arg-parser #'parse-integer
|
||||
:meta-var "PORT")
|
||||
(:name :config
|
||||
:description "specify the config to use"
|
||||
:short #\c
|
||||
:long "config"
|
||||
:arg-parser #'string
|
||||
:meta-var "FILE")
|
||||
(:name :prod
|
||||
:description "enabled production mode"
|
||||
:long "production")
|
||||
(:name :version
|
||||
:description "prints application version"
|
||||
:long "version"))
|
||||
|
||||
(defvar *handler* nil)
|
||||
|
||||
(defun start (&rest args &key server port debug &allow-other-keys)
|
||||
(declare (ignore server port debug))
|
||||
(when *handler*
|
||||
(restart-case (error "Server is already running.")
|
||||
(restart-server ()
|
||||
:report "Restart the server"
|
||||
(stop))))
|
||||
(setf *handler*
|
||||
(apply #'clackup (ida-bot.app:get-builder) args)))
|
||||
|
||||
(defun stop ()
|
||||
(prog1
|
||||
(clack:stop *handler*)
|
||||
(setf *handler* nil)))
|
||||
|
||||
(defun make-start ()
|
||||
"our binary entry point"
|
||||
(multiple-value-bind (opts args) (get-opts)
|
||||
(when (getf opts :version)
|
||||
(format t "ida-bot v~A~&" #.(asdf:component-version (asdf:find-system :ida-bot)))
|
||||
(uiop:quit 0))
|
||||
|
||||
(when (getf opts :help)
|
||||
(unix-opts:describe
|
||||
:usage-of "idabot")
|
||||
(uiop:quit 0))
|
||||
|
||||
(when (getf opts :prod)
|
||||
(setf (uiop:getenv "APP_ENV") "production"))
|
||||
|
||||
(if (getf opts :config)
|
||||
(conf:load-config (getf opts :config) :parse-lists nil)
|
||||
(progn
|
||||
(format t "please specify a config file to use")
|
||||
(uiop:quit 1)))
|
||||
|
||||
(ida-bot.extension:load-commands)
|
||||
|
||||
(let ((server #+(and Unix SBCL) :woo
|
||||
#-(and Unix SBCL) :hunchentoot))
|
||||
|
||||
(start :port (env :port (or (getf opts :port) 9000)) :server server)
|
||||
|
||||
(handler-case
|
||||
(with-user-abort
|
||||
(bt:join-thread (find-if (lambda (th)
|
||||
(search (string-downcase (string server)) (bt:thread-name th)))
|
||||
(bt:all-threads))))
|
||||
(user-abort ())
|
||||
(error (c) (format t "Woops, an unknown error occured:~&~a~&" c)))
|
||||
|
||||
(format t "~&Quitting bot.")
|
||||
(stop))))
|
|
@ -0,0 +1,42 @@
|
|||
(defpackage :ida-bot.actions
|
||||
(:use :cl :ida-bot.util)
|
||||
|
||||
(:import-from :ida-bot.config :env)
|
||||
(:import-from :drakma :http-request)
|
||||
(:import-from :com.inuoe.jzon :stringify)
|
||||
(:import-from :alexandria :alist-hash-table)
|
||||
|
||||
(:export
|
||||
|
||||
:send-chat
|
||||
:moderate-chat
|
||||
:moderate-user))
|
||||
(in-package :ida-bot.actions)
|
||||
|
||||
(defun send-chat (message)
|
||||
"sends a chat message to the configured server"
|
||||
(owncast-request "/api/integrations/chat/send"
|
||||
`(("body" . ,message))))
|
||||
|
||||
|
||||
(defun moderate-chat (message-id set-visible)
|
||||
"moderates a chat message with id MESSAGE-ID, setting it visible based on SET-VISIBLE"
|
||||
(owncast-request "/api/chat/messagevisibility"
|
||||
`(("visible" . ,set-visible)
|
||||
("idArray" . (,message-id)))))
|
||||
|
||||
(defun moderate-user (user-id set-enabled)
|
||||
"moderates a user with USER-ID, enabling them based on SET-ENABLED"
|
||||
(owncast-request "/api/chat/users/setenabled"
|
||||
`(("userId" . ,user-id)
|
||||
("enabled" . ,set-enabled))))
|
||||
|
||||
(defun owncast-request (api-path payload)
|
||||
"runs the owncast request"
|
||||
(http-request (str:concat (env :stream-url) api-path)
|
||||
:method :post
|
||||
:content-type "application/json"
|
||||
:accept "application/json"
|
||||
:content (stringify (alist-hash-table payload))
|
||||
:additional-headers `(("Authorization" . ,(str:concat "Bearer " (env :access-token))))))
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
(in-package :cl-user)
|
||||
(defpackage ida-bot.util
|
||||
(:use :cl)
|
||||
(:import-from :bt
|
||||
:make-thread)
|
||||
(:export
|
||||
|
||||
:agetf
|
||||
:check-type-symbol
|
||||
:->))
|
||||
|
||||
(in-package ida-bot.util)
|
||||
|
||||
(declaim (inline agetf))
|
||||
|
||||
(defun agetf (place indicator &optional default)
|
||||
(or (cdr (assoc indicator place :test #'equal))
|
||||
default))
|
||||
|
||||
(defun check-type-symbol (tsym tstr)
|
||||
(or (and (eq tsym :chat) (string= tstr "CHAT"))
|
||||
(and (eq tsym :name-changed) (string= tstr "NAME_CHANGED"))
|
||||
(and (eq tsym :user-joined) (string= tstr "USER_JOINED"))
|
||||
(and (eq tsym :stream-started) (string= tstr "STREAM_STARTED"))
|
||||
(and (eq tsym :stream-stopped) (string= tstr "STREAM_STOPPED"))
|
||||
(and (eq tsym :visibility-update) (string= tstr "VISIBILITY-UPDATE"))))
|
||||
|
||||
(defmacro -> ((&key name) &body body)
|
||||
(let ((n (or name (string (gensym)))))
|
||||
`(bt:make-thread #'(lambda () ,@body) :name ,n)))
|
|
@ -0,0 +1,36 @@
|
|||
(in-package :cl-user)
|
||||
(defpackage ida-bot.web
|
||||
(:use :cl
|
||||
:caveman2)
|
||||
(:import-from :ida-bot.util
|
||||
:agetf)
|
||||
(:import-from :ida-bot.commands
|
||||
:process-commands)
|
||||
(:export :*web*))
|
||||
(in-package :ida-bot.web)
|
||||
|
||||
;; for @route annotation
|
||||
(syntax:use-syntax :annot)
|
||||
|
||||
;;
|
||||
;; Application
|
||||
|
||||
(defclass <web> (<app>) ())
|
||||
(defvar *web* (make-instance '<web>))
|
||||
(clear-routing-rules *web*)
|
||||
|
||||
;;
|
||||
;; Routing rules
|
||||
|
||||
@route POST "/ingest"
|
||||
(defun parse-webhooks (&key _parsed)
|
||||
(process-commands _parsed))
|
||||
|
||||
|
||||
;;
|
||||
;; Error pages
|
||||
|
||||
(defmethod on-exception ((app <web>) (code (eql 404)))
|
||||
(declare (ignore app))
|
||||
(merge-pathnames #P"_errors/404.html"
|
||||
*template-directory*))
|
|
@ -0,0 +1,12 @@
|
|||
(in-package :cl-user)
|
||||
(defpackage ida-bot-test
|
||||
(:use :cl
|
||||
:ida-bot
|
||||
:prove))
|
||||
(in-package :ida-bot-test)
|
||||
|
||||
(plan nil)
|
||||
|
||||
;; blah blah blah.
|
||||
|
||||
(finalize)
|
Loading…
Reference in New Issue