maiden é uma coleção de sistemas para ajudá-lo a construir aplicativos e bibliotecas que interagem com servidores de chat. Pode ajudá-lo a construir um bot de bate-papo ou um cliente de bate-papo geral. Ele também oferece uma variedade de partes que devem tornar muito mais fácil escrever um cliente para um novo protocolo de chat.
Se você só se preocupa em usar o maiden para configurar algum tipo de bot, as etapas para fazer isso são bastante diretas. Primeiro, queremos carregar o maiden e todos os módulos e componentes que você gostaria de usar no seu bot.
(ql:quickload '( maiden maiden -irc maiden -commands maiden -silly))
E então criaremos um núcleo com instâncias dos consumidores adicionadas como gostaríamos que fossem.
(defvar *core* ( maiden :make-core
'(: maiden -irc :nickname " maiden Test" :host "irc.freenode.net" :channels ("##testing"))
: maiden -commands
: maiden -silly))
O comando make-core usa nomes de pacotes (como strings ou símbolos) de consumidores para adicionar ou o nome direto da classe de um consumidor. No primeiro caso, ele tentará encontrar sozinho o nome da classe de consumidor apropriada.
E é isso. make-core
irá criar um núcleo, instanciar todos os consumidores, adicioná-los a ele e iniciar tudo. Muitos dos módulos fornecidos para maiden farão uso de algum tipo de configuração ou armazenamento persistente. Para seu gerenciamento, consulte o subsistema de armazenamento.
Para usar maiden como framework, primeiro você desejará definir seu próprio sistema e pacote como de costume para um projeto. Por enquanto, usaremos apenas o pacote maiden -user
para brincar. A seguir, definiremos um consumidor. Isso pode ser feito com define-consumer
.
(in-package #: maiden -user)
(define-consumer ping-notifier (agent)
())
Normalmente você desejará definir um agente. Os agentes só podem existir uma vez em um núcleo. Veremos um exemplo para um cliente mais tarde. Agora, de agora em diante, podemos definir nossos próprios métodos e funções que se especializam ou atuam na classe consumidora, como você estaria acostumado na programação CLOS geral. A seguir, definiremos nosso próprio evento que usaremos para enviar “solicitações de ping” ao sistema.
(define-event ping (passive-event)
())
O evento é definido como um passive-event
, pois não solicita diretamente a execução de uma ação, mas informa ao sistema sobre um ping que está acontecendo. Agora, para realmente fazer o consumidor interagir com o sistema de eventos, também queremos definir manipuladores. Isso pode ser feito com define-handler
.
(define-handler (ping-notifier ping-receiver ping) (c ev)
(v:info :ping "Received a ping: ~a" ev))
Isso define um manipulador chamado ping-receiver
em nosso consumidor ping-notifier
. Ele também especifica que escutará eventos do tipo ping
. A lista de argumentos posteriormente diz que a instância do consumidor está vinculada a c
e a instância do evento a ev
. O corpo então simplesmente registra uma mensagem informativa usando Verbose.
Vamos testar isso bem rápido.
(defvar *core* (make-core 'ping-notifier))
(do-issue *core* ping)
Isso deve imprimir a mensagem de status no REPL conforme esperado. E isso é quase tudo que existe para usar este sistema. Observe que, para fazer coisas realmente úteis, você provavelmente desejará usar alguns dos subsistemas preexistentes que o projeto maiden oferece além do núcleo. Eles irão ajudá-lo com usuários, canais, contas, comandos, rede, armazenamento e assim por diante. Lembre-se também de que você também pode usar os recursos que o Deeds oferece por conta própria, como filtrar expressões para manipuladores.
Agora vamos dar uma olhada em um tipo primitivo de cliente. O cliente simplesmente poderá gravar em um arquivo por meio de eventos.
(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.")))
Tornamos o write-event
um client-event
pois ele precisa ser específico para um cliente para o qual queremos escrever, e o transformamos em um active-event
pois solicita que algo aconteça. Agora vamos definir nosso manipulador que se encarregará de realmente escrever a sequência no arquivo.
(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)))
A opção :match-consumer
modifica o filtro do manipulador de tal forma que o filtro passará apenas eventos cujo slot client
contém a mesma instância file-client
à qual pertence a instância do manipulador atual. Isso é importante, pois cada instância do file-client
receberá suas próprias instâncias de seus manipuladores em um núcleo. Sem esta opção, o write-event
seria tratado por todas as instâncias do file-client
independentemente da instância à qual o evento se destinava. Observe também que adicionamos um argumento sequence
à lista de argumentos do manipulador. Este argumento será preenchido com o slot apropriado do evento. Se nenhum slot for encontrado, um erro será sinalizado.
É hora de testar. Vamos apenas reutilizar o núcleo de cima.
(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"
Como você pode ver, os eventos foram direcionados para as instâncias de manipulador apropriadas de acordo com o cliente que queríamos e, portanto, os arquivos contêm o que esperamos que eles contenham.
Por fim, vale ressaltar que também é possível adicionar e remover manipuladores dinamicamente em tempo de execução, e até mesmo fazê-lo para manipuladores que não estejam associados a um determinado consumidor. Isso geralmente é útil quando você precisa aguardar um evento de resposta de algum lugar. Para lidar com a lógica de fazer isso de forma assíncrona e manter a impressão de um fluxo imperativo, maiden oferece - assim como o Deeds - uma macro with-awaiting
. Pode ser usado da seguinte forma:
(with-awaiting (core event-type) (ev some-field)
(do-issue core initiating-event)
:timeout 20
some-field)
with-awaiting
é muito semelhante a define-handler
, com a exceção de que não leva um nome e, em vez de um nome de consumidor no início, precisa de uma instância central ou de consumidor. Também é necessária uma opção extra que não seria utilizada, a :timeout
. Outro extra necessário é o "formulário de configuração" após a lista de argumentos. Para gerenciar tudo adequadamente e garantir que nenhuma condição de corrida possa ocorrer no sistema, você deve iniciar o processo que solicitará o eventual evento de resposta neste formulário de configuração. Se você iniciá-lo antes disso, o evento de resposta poderá ser enviado antes que o manipulador temporário seja configurado no sistema e parecerá que nunca chegou.
E isso é praticamente todo o básico. Conforme mencionado acima, dê uma olhada nos subsistemas que este projeto inclui, pois eles irão ajudá-lo com todos os tipos de tarefas e problemas comuns relacionados a sistemas de chat e assim por diante.
Antes de entender maiden , vale a pena entender Deeds, mesmo que apenas superficialmente. maiden baseia-se bastante nisso.
Um core
é a parte central de uma configuração maiden . É responsável por gerenciar e orquestrar os demais componentes do sistema. Você pode ter vários núcleos rodando simultaneamente na mesma imagem lisp e pode até compartilhar componentes entre eles.
Mais especificamente, um Core é composto por um loop de eventos e um conjunto de consumidores. O loop de eventos é responsável por entregar eventos aos manipuladores. Os consumidores são responsáveis por anexar manipuladores ao loop de eventos. As operações que você provavelmente desejará realizar em um núcleo são: emitir eventos para ele por issue
, adicionar consumidores a ele por add-consumer
, ou remover um consumidor dele por remove-consumer
.
Para facilitar a criação de um núcleo útil com consumidores adicionados a ele, você pode usar as funções make-core
e add-to-core
.
Um event
é um objeto que representa uma mudança no sistema. Os eventos podem ser usados para representar uma mudança que ocorreu ou para representar uma solicitação para que uma mudança aconteça. Eles são chamados passive-event
e active-event
respectivamente.
Geralmente você usará eventos das seguintes maneiras:
Um consumer
é uma classe que representa um componente do sistema. Cada consumidor pode ter uma infinidade de manipuladores vinculados a ele, que reagirão aos eventos do sistema. Os consumidores vêm em dois supertipos básicos, agent
e client
. Agentes são consumidores que só deveriam existir uma vez em um núcleo, pois implementam funcionalidades que não fariam sentido serem multiplexadas de alguma forma. Os clientes, por outro lado, representam algum tipo de ponte para um sistema externo e, naturalmente, deveriam ter permissão para ter múltiplas instâncias no mesmo núcleo.
Assim, o desenvolvimento de um conjunto de comandos ou de algum tipo de interface provavelmente levaria a um agente, enquanto a interface com um serviço como o XMPP levaria a um cliente.
A definição de um consumidor deve acontecer com define-consumer
, que é semelhante ao padrão defclass
, mas garante que as superclasses e metaclasses estejam configuradas corretamente.
handler
são objetos que contêm uma função que executa certas ações quando um evento específico é emitido no núcleo. Cada manipulador está vinculado a um consumidor específico e é removido ou adicionado ao loop de eventos do núcleo quando o consumidor é removido ou adicionado ao núcleo.
A definição do manipulador acontece por meio de define-handler
, define-function-handler
, define-instruction
ou define-query
. Que cada um se baseia sucessivamente no último para fornecer uma abreviatura mais ampla para requisitos comuns. Observe que a maneira como um manipulador realmente recebe seus eventos pode ser diferente. Dê uma olhada na documentação do Deeds para ver quais classes de manipulador estão disponíveis.
Incluídos no projeto maiden estão alguns subsistemas que estendem a funcionalidade principal.
O projeto maiden também inclui alguns clientes padrão que podem ser usados imediatamente.
Por fim, o projeto possui vários módulos de agente que fornecem funcionalidades úteis para a criação de chatbots e outros. Eles também podem ser usados imediatamente.