Git Product home page Git Product logo

maiden's Introduction

About Maiden

Maiden is a collection of systems to help you build applications and libraries that interact with chat servers. It can help you build a chat bot, or a general chat client. It also offers a variety of parts that should make it much easier to write a client for a new chat protocol.

How To Use Maiden as a Bot

If you only care about using Maiden to set up a bot of some kind, the steps to do so are rather straightforward. First we'll want to load in Maiden and all of the modules and components that you'd like to use in your bot.

(ql:quickload '(maiden maiden-irc maiden-commands maiden-silly))

And then we'll create a core with instances of the consumers added to it as we'd like them to be.

(defvar *core* (maiden:make-core
                 '(:maiden-irc :nickname "MaidenTest" :host "irc.freenode.net" :channels ("##testing"))
                 :maiden-commands
                 :maiden-silly))

The make-core command takes either package names (as strings or symbols) of consumers to add, or the direct class name of a consumer. In the former case it'll try to find the appropriate consumer class name on its own.

And that's it. make-core will create a core, instantiate all the consumers, add them to it, and start everything up. A loot of the modules provided for Maiden will make use of some kind of configuration or persistent storage. For the management thereof, see the storage subsystem.

How To Use Maiden as a Framework to Develop With

In order to use Maiden as a framework, you'll first want to define your own system and package as usual for a project. For now we'll just use the maiden-user package to play around in. Next we'll want to define a consumer. This can be done with define-consumer.

(in-package #:maiden-user)
(define-consumer ping-notifier (agent)
  ())

Usually you'll want to define an agent. Agents can only exist once on a core. We'll go through an example for a client later. Now, from here on out we can define our own methods and functions that specialise or act on the consumer class as you'd be used to from general CLOS programming. Next, we'll define our own event that we'll use to send "ping requests" to the system.

(define-event ping (passive-event)
  ())

The event is defined as a passive-event as it is not directly requesting an action to be taken, but rather informs the system of a ping that's happening. Now, in order to actually make the consumer interact with the event system however, we'll also want to define handlers. This can be done with define-handler.

(define-handler (ping-notifier ping-receiver ping) (c ev)
  (v:info :ping "Received a ping: ~a" ev))

This defines a handler called ping-receiver on our ping-notifier consumer. It also specifies that it will listen for events of type ping. The arglist afterwards says that the consumer instance is bound to c and the event instance to ev. The body then simply logs an informational message using Verbose.

Let's test this out real quick.

(defvar *core* (make-core 'ping-notifier))

(do-issue *core* ping)

That should print the status message to the REPL as expected. And that's most of everything there is to using this system. Note that in order to do actually useful things, you'll probably want to make use of some of the preexisting subsystems that the Maiden project delivers aside from the core. Those will help you with users, channels, accounts, commands, networking, storage, and so forth. Also keep in mind that you can make use of the features that Deeds offers on its own as well, such as filtering expressions for handlers.

Now let's take a look at a primitive kind of client. The client will simply be able to write to a file through events.

(define-consumer file-client (client)
  ((file :initarg :file :accessor file))
  (:default-initargs :file (error "FILE required.")))
  
(define-event write-event (client-event active-event)
  ((sequence :initarg :sequence))
  (:default-initargs :sequence (error "SEQUENCE required.")))

We've made the write-event a client-event since it needs to be specific to a client we want to write to, and we've made it an active-event since it requests something to happen. Now let's define our handler that will take care of actually writing the sequence to file.

(define-handler (file-client writer write-event) (c ev sequence)
  :match-consumer 'client
  (with-open-file (stream (file c) :direction :output :if-exists :append :if-does-not-exist :create)
    (write-sequence sequence stream)))

The :match-consumer option modifies the handler's filter in such a way that the filter will only pass events whose client slot contains the same file-client instance as the current handler instance belongs to. This is important, as each instance of file-client will receive its own instances of its handlers on a core. Without this option, the write-event would be handled by every instance of the file-client regardless of which instance the event was intended for. Also note that we added a sequence argument to the handler's arglist. This argument will be filled with the appropriate slot from the event. If no such slot could be found, an error is signalled.

Time to test it out. We'll just reuse the core from above.

(add-to-core *core* '(file-client :file "~/foo" :name :foo)
                    '(file-client :file "~/bar" :name :bar))

(do-issue *core* write-event :sequence "foo" :client (consumer :foo *core*))
(do-issue *core* write-event :sequence "bar" :client (consumer :bar *core*))

(alexandria:read-file-into-string "~/foo") ; => "foo"
(alexandria:read-file-into-string "~/bar") ; => "bar"

As you can see, the events were directed to the appropriate handler instances according to the client we wanted, and the files thus contain what we expect them to.

Finally, it is worth mentioning that it is also possible to dynamically add and remove handlers at runtime, and even do so for handlers that are not associated with a particular consumer. This is often useful when you need to wait for a response event from somewhere. To handle the logic of doing this asynchronously and retain the impression of an imperative flow, Maiden offers --just as Deeds does-- a with-awaiting macro. It can be used as follows:

(with-awaiting (core event-type) (ev some-field)
    (do-issue core initiating-event)
  :timeout 20
  some-field)

with-awaiting is very similar to define-handler, with the exception that it doesn't take a name, and instead of a consumer name at the beginning it needs a core or consumer instance. It also takes one extra option that is otherwise unused, the :timeout. Another required extra is the "setup form" after the arglist. In order to properly manage everything and ensure no race conditions may occur in the system, you must initiate the process that will prompt the eventual response event in this setup form. If you initiate it before then, the response event might be sent out before the temporary handler is set up in the system and it'll appear as if it never arrived at all.

And that's pretty much all of the basics. As mentioned above, take a look at the subsystems this project includes, as they will help you with all sorts of common tasks and problems revolving around chat systems and so on.

Core Documentation

Before understanding Maiden, it is worth it to understand Deeds, if only at a surface level. Maiden builds on it rather heavily.

Core

A core is the central part of a Maiden configuration. It is responsible for managing and orchestrating the other components of the system. You can have multiple cores running simultaneously within the same lisp image, and can even share components between them.

More specifically, a Core is made up of an event-loop and a set of consumers. The event-loop is responsible for delivering events to handlers. Consumers are responsible for attaching handlers to the event-loop. The operations you will most likely want to perform on a core are thus: issuing events to it by issue, adding consumers to it by add-consumer, or removing a consumer from it by remove-consumer.

In order to make it easier on your to create a useful core with consumers added to it, you can make use of the make-core and add-to-core functions.

Event

An event is an object that represents a change in the system. Events can be used to either represent a change that has occurred, or to represents a request for a change to happen. These are called passive-events and active-events respectively.

Generally you will use events in the following ways:

  1. Consuming them by writing a handler that takes events of a particular type and does something in response to them.
  2. Define new event classes that describe certain behaviour.
  3. Emitting them by writing components that inform the system about changes.

Consumer

A consumer is a class that represents a component in the system. Each consumer can have a multitude of handlers tied to it, which will react to events in the system. Consumers come in two basic supertypes, agents and clients. Agents are consumers that should only exist on a core once, as they implement functionality that would not make sense to be multiplexed in some way. Clients on the other hand represent some kind of bridge to an outside system, and naturally should be allowed to have multiple instances on the same core.

Thus developing a set of commands or an interface of some kind would probably lead to an agent, whereas interfacing with a service like XMPP would lead to a client.

Defining a consumer should happen with define-consumer, which is similar to the standard defclass, but ensures that the superclasses and metaclasses are properly set up.

Handler

handlers are objects that hold a function that performs certain actions when a particular event is issued onto the core. Each handler is tied to a particular consumer and is removed or added to the core's event-loop when the consumer is removed or added to the core.

Handler definition happens through one of define-handler, define-function-handler, define-instruction, or define-query. Which each successively build on the last to provide a broader shorthand for common requirements. Note that the way in which a handler actually receives its events can differ. Have a look at the Deeds' documentation to see what handler classes are available.

Subsystems

Included in the Maiden project are a couple of subsystems that extend the core functionality.

  • API Access -- Helper functions to access remote APIs over HTTP.
  • Client Entities -- Common entities for clients and remote systems.
  • Networking -- Client mixins to handle network connections.
  • Serialize -- Data to wire serialization.
  • Storage -- Configuration and data storage.

Existing Clients

The Maiden project also includes a few standard clients that can be used right away.

Existing Agents

Finally, the project has a bunch of agent modules that provide functionality that is useful for creating chat bots and such. They, too, can be used straight away.

  • accounts -- User accounts.
  • activatable -- Allow activating or deactivating consumer's handlers.
  • blocker -- Block commands by rules.
  • chatlog -- Log channels to a database.
  • commands -- Provide a common command infrastructure.
  • core-manager -- Manage the core's consumers and parts.
  • counter -- Count occurrences in message events.
  • crimes -- Lets the user play "Crimes", a Cards Against Humanity clone.
  • emoticon -- Provides :emote:s in messages.
  • help -- A generic command help system.
  • location -- Allows retrieving location information.
  • markov -- Provides efficient markov chains.
  • medals -- Gives commands to hand out "medals" to users.
  • notify -- Allow sending users notification messages.
  • permissions -- Manage command permissions.
  • quicklisp -- Operate Quicklisp and system updates through commands.
  • silly -- Adds some silly commands and automatic message responses.
  • talk -- Allows playing Text To Speech messages on the machine Maiden runs on.
  • throttle -- Throttle users to prevent them from issuing too many commands.
  • time -- Provides time information.
  • trivia -- Implements a simple trivia game.
  • urlinfo -- Retrieves information about URLs.
  • weather -- Provides weather and forecast information for a location.

maiden's People

Contributors

no-defun-allowed avatar shinmera avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

maiden's Issues

Some systems failed to build for Quicklisp dist

Building with SBCL 2.1.6 / ASDF 3.3.1 for quicklisp dist creation.

Trying to build commit id a841bf3

maiden-twitter fails to build with the following error:

Unhandled SB-PCL:CLASS-NOT-FOUND-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING {1001A18103}>: There is no class named CHIRP-OBJECTS:CONFIGURATION.

Full log here

Handle CTCP and DCC

These protocols are currently completely unsupported.

In the case of CTCP, the PRIVMSG events that contain CTCP stuff must be intercepted, cancelled, and then re-issued as a CTCP-superclassed event that fits whatever the CTCP event is. This is necessary because otherwise the PRIVMSG events would get doubly processed by things like loggers, which is undesired.

In case of DCC we probably want some kind of collection of events to handle the various things that can happen. Given that I have no fucking idea about what DCC is capable of, I can't really give much insight on this.

Some systems failed to build for Quicklisp dist

Building with SBCL 2.0.5 / ASDF 3.3.1 for quicklisp dist creation.

Trying to build commit id 7c33f09

maiden-chatlog fails to build with the following error:

; caught ERROR:
;   READ error during COMPILE-FILE: Package BABEL does not exist. Line: 81, Column: 57, File-Position: 3163 Stream: #<SB-INT:FORM-TRACKING-STREAM for "file /home/quicklisp/quicklisp-controller/dist/build-cache/maiden/8fc5b84752f4c79b9f43bb2a201fde1e8d921233/maiden-20210528-git/agents/chatlog/chatlog.lisp" {10338F5243}>
...
Unhandled UIOP/LISP-BUILD:COMPILE-FILE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING {1000A18083}>: COMPILE-FILE-ERROR while compiling #<CL-SOURCE-FILE "maiden-chatlog" "chatlog">

Full log here

Persist Users and Channels

Currently, the IRC client only deals with users and channels as strings. This is inconvenient. Instead, they should be user and channel objects that can be worked with.

There is some trickery involved here, because of the way IRC works. When you are not in the same channel as a user, you basically know nothing about them. If you receive a PRIVMSG from someone and a second later you receive another PRIVMSG from what looks like the same person, those two messages could come from two different people.

Only if you are in the same channel as a user, you receive updates of them parting, quitting, or changing nicknames. Those updates allow you to keep track of them as specific users.

There's more to it. Replies can also come from servers instead of users. See the grammar in the RFC. There is no perfect way to distinguish between them, but we can get far:

  • Servers often send not just the nickname of a user, but also their username and hostname (nick!user@host). This is invalid for servers, so if they are there we are dealing with a user.
  • Nicknames are also allowed to contain more characters than server names---[, ], \, ```, _, `^`, `{`, `|`, `}`---so if such a character exists we are also dealing with a user.
  • Similarly, user nicknames are not allowed to contain dots. Server hostnames often will, so if there is a . we know we're dealing with a server.

In the ambiguous case, where the message prefix consists of only a nickname or a servername without a dot anywhere, a choice will have to be made whether or not to consider the sender to be a server or a user. If we know of a user with that nickname, that's who we're dealing with. Otherwise all bets are off.

I am under the impression that all IRC servers send full usernames and hostnames in reply prefixes. If that's true---we could do a quick survey to verify---we can default to treating ambiguous names as server names.

The above is based on my knowledge of the RFC. I may have missed or forgotten something, in which case it could all be much easier than I think.

Some systems failed to build for Quicklisp dist

Building with SBCL 2.0.5 / ASDF 3.3.1 for quicklisp dist creation.

Trying to build commit id fb789c9

maiden-lichat fails to build with the following error:

; caught WARNING:
;   Duplicate definition for LICHAT-CMD::EMOTE found in one file.
;   See also:
;     The ANSI Standard, Section 3.2.2.3
...
Unhandled UIOP/LISP-BUILD:COMPILE-FILE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING {1000A18083}>: COMPILE-FILE-ERROR while compiling #<CL-SOURCE-FILE "maiden-lichat" "events">

Full log here

Some systems failed to build for Quicklisp dist

Building with SBCL 2.2.0.166-863804f68 / ASDF 3.3.5 for quicklisp dist creation.

Trying to build commit id 5ee0a6e

maiden-lichat fails to build with the following error:

; caught ERROR:
;   READ error during COMPILE-FILE: Symbol "WIRE-OBJECT" not found in the LICHAT-PROTOCOL package. Line: 64, Column: 60, File-Position: 3162 Stream: #<SB-INT:FORM-TRACKING-STREAM for "file /home/quicklisp/quicklisp-controller/dist/build-cache/maiden/407012a1b8a49890ffae777c681fc8aaefee8459/maiden-20220131-git/clients/lichat/events.lisp" {1012D5CBE3}>
...
Unhandled UIOP/LISP-BUILD:COMPILE-FILE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING {1001B98003}>: COMPILE-FILE-ERROR while compiling #<CL-SOURCE-FILE "maiden-lichat" "events">

Full log here

Pool Handlers on Consumer

Currently each handler defaults to a threaded handler with a single thread. However, since we have a few handlers per consumer, and quite a few consumers in a running instance, this quickly adds up to hundreds of threads. It would be better if the handlers would share a thread with the consumer instance by default.

Perhaps this could be facilitated by making each consumer its own event-loop with a backing thread to which the consumers are added, rather than adding the consumers to the core event loop directly.

Startup of Cores and Consumers Is Weird

There's some weird kludgery going on where things don't get properly restored if the core is started up after trying to add a consumer that isn't started up either (which will fail half-way). Possibly things should even be started up automatically as soon as they're instantiated, though I don't know how I feel about that at the moment. Either way, this underlying issue needs to be resolved.

Possibly also affects Shinmera/deeds

IRC Ban Management

Tracking the age of bans would be useful to clean up banlists. Automated ban management for a channel would also be nice (clear up to, clear oldest on add, etc.)

Might also want to think about a client-agnostic protocol for bans.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.