Forum Programmation.ruby Système de plugins

Posté par  . Licence CC By‑SA.
Étiquettes : aucune
0
17
août
2013

Bonjour à tous,

Je m'amuse à faire un bot IRC (pas très original, je sais).
Le but c'est de le faire le plus simple possible et pouvoir
lui ajouter des fonctionnalités avec des scripts/plugins.

Je sais pas trop comment faire le lien entre le bot et ses plugins
Je dois pouvoir faire 2 choses:
1. associer des méthodes à un événement (type du message provenant du serveur)
2. ajouter des commandes pour contrôler le bot.

J'ai cherché mais pas trouvé grand chose, pour le moment je fais çà:

class Plugin
  @@registered = []
  def self.registered
    @@registered
  end

  def self.inherited(subclass)
    @@registered << subclass.new
  end
end

class Test < Plugin
  # répond au ping
  def onPing(bot, msg)
    bot.pong(msg.params[0])
  end

  # Exemple d'une commande
  # là je pense qu'il faudrait que j'utilise OptionParser
  # ou un module équivalent pour pouvoir executer des
  # commandes avec paramètres plus facilement.
  def doHelp(bot, msg)
    if msg.params.last =~ /^!help (\S+)/
      ...
    end
  end
end

class Bot
  def run
    # lit sur la socket
    # parse les données reçues
    msg = Message.parse(data)
    event = "on#{msg.command.capitalize}"
    Plugin.registered.each do |pl|
      # parcours toutes les instances de Plugin
      # et vérifie si une méthode on<Event> existe
      if pl.respond_to?(event)
        pl.send(event, self, msg)
      end
    end
    if msg.command == "PRIVMSG"
      # message reçu sur un channel ou en privé
      # vérifier si c'est une commande du bot
      # préfixée par un '!' ou adressée directement au bot.
      ...
    end
  end
end

ou bien, juste utiliser un hash avec comme clé le type d'événement et comme valeur, la méthode associée.
mais un événement peut avoir plusieurs méthodes à appeler.
puis certaines prendront plus de temps que d'autres et devront être exécutées dans un thread ?

bref je suis un peu paumé, si vous avez quelques conseils, liens qui pourraient m'aider pour faire çà proprement.

  • # Cinch

    Posté par  . Évalué à 1.

    Cinch permet la création de bot IRC en Ruby, tu dois pouvoir jeter un œil sur son système de chargement des extensions.

    • [^] # Re: Cinch

      Posté par  . Évalué à 0.

      Je vais regarder çà, merci!

  • # Parcourir une liste de plugins

    Posté par  . Évalué à 0. Dernière modification le 17 août 2013 à 14:28.

    Je ne code pas en Ruby, donc je ne saurai faire l'implémentation de ce que je vais te proposer.

    Ce que je ferais en tout cas, c'est lister les plugins disponibles (donc par exemple un fichier -> un plugin avec une classe bien nommée etc…), puis les méthodes de chacun avec une méthode particulière qui va permettre par exemple de « recevoir » les messages entrants/sortants, les traiter et finir par les refourger à qui doit l'avoir.

    Avec les méthodes d'introspection de ces langages, la tâche n'est pas bien difficile.

    Socket -> réception de message -> traitement par l'application -> traitement par les plugins -> retour du message traité (pas forcément modifié) à l'application -> cours normal des choses.

    Le traitement des plugins ça peut être d'envoyer quelque chose automatiquement, de faire une notification via D-Bus, ou de modifier le message entrant, que sais-je.

    Et on peut aussi bien imaginer une méthode qui va dans le sens inverse : modification de la saisie d'un message d'un utilisateur pour faire de la correction orthographique automatique par exemple.

    Et si tu juges qu'un traitement est long, c'est alors au plugin de se « threader ».

    Voilà ma vision des choses.

    • [^] # Re: Parcourir une liste de plugins

      Posté par  . Évalué à 0.

      Merci, çà m'aide beaucoup.
      çà me plaît bien le traitement en sens inverse pour filtrer les messages entrants et sortants et du coup il faudrait un ordre de priorité pour charger les plugins/les parcourir.

      • [^] # Re: Parcourir une liste de plugins

        Posté par  . Évalué à 0.

        Ouais, tu peux faire de manière relative, genre je mets arbitrairement 100 alors que j'ai un plugin qui demande la priorité 10 quoi.

        Après on peut rentrer des des étages plus complexes genre les plugins de prio 0 à 100 sont prévus pour faire des actions importantes et longues alors que les plugins de 200 à 300 sont plus légers.

        On peut aussi imaginer un gestionnaire de conflit, genre tel ou tel plugin doit absolument passer avant/après tel autre. Bonjour les dépendances 8D

        De toute façon il n'y a de limite que l'imagination :)

  • # Pour ma part, je ne passerais pas par des classes mais par des mixin

    Posté par  . Évalué à 2.

    Je ne suis pas sur d'avoir compris exactement ce que tu veux faire, mais je pense que dans ce cas ce serait plus approprié.

    En nommant ton plugin du nom de ton event, tu peux le charger dynamiquement lors de la réception de l'evenement.

    http://www.tutorialspoint.com/ruby/ruby_modules.htm

    • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

      Posté par  . Évalué à 2.

      J'avais oublié également la possibilité de passer par la méthode method_missing. Sinon, si Ruby t'intéresse, je te conseille vivement le bouquin les design pattern en ruby. Tu y trouveras plein de petites astuces que tu ne trouves pas forcément ailleurs et qui te permettront de comprendre la philosophie qui est derrière la programmation Ruby.

      • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

        Posté par  . Évalué à 2. Dernière modification le 17 août 2013 à 16:44.

        J'ai pas pu m'empêcher de tester … Méchant, j'avais d'autres choses à faire … :)

        Voici la piste que je prendrais :

        #!/usr/pkg/bin/ruby
        
        module Event1
            def self.onEvent1()
                puts("Event 1 deected")
            end
        end
        
        
        class Test
        
        
            def method_missing(name, *args)
                puts("Method missing : #{name}, args: #{args}")
                mod=name.to_s().sub("on","")
                code=%Q{#{mod}.#{name.to_s()}}
                puts(code)
                begin 
                    eval(code)
                rescue NameError
                    puts("Module  #{mod} does not exists")
                end
            end
        
        end
        
        t=Test.new()
        
        t.onEvent1()
        t.onEvent2()
        

        Le module event2 n'existe pas donc tu auras une erreur interceptée par begin/rescue (traitement des exceptions en Ruby).

        Il y a une partie de metaprogrammation pour appeler le bon module, je peux expliquer si tu comprends pas (mais là j'ai pas le temps, peut-être ce soir si ça t'intéresse).

        En tout cas si tu arrives à comprendre ça, et que ça peut t'aider à smplifier ton code, tant mieux.

        • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

          Posté par  . Évalué à 0.

          Désolé mais je vais encore te faire perdre ton temps,
          je comprends pas :p

          Si j'ai un module qui correspond à un type d'événement, comment je fais pour exécuter plusieurs méthodes sur cet événement dans différents plugins? je sais pas si je suis bien clair.

          En gros, je reçois un message de type « JOIN » (joindre un salon).

          # plugin1.rb
          module JOIN
            def self.onJoin
            end
          end
          # plugin2.rb
          module JOIN
            # je définis un autre nom de méthode?
            def self.onJoin
            end
          end
          # main.rb
          class Bot
            def run
              # parsing
              # msg.command == "JOIN"
              # exécuter les méthodes "onJOIN" des plugins.
            end
          end

          Merci en tous les cas.

          • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

            Posté par  . Évalué à 2. Dernière modification le 17 août 2013 à 21:19.

            Si j'ai un module qui correspond à un type d'événement, comment je fais pour exécuter plusieurs méthodes sur cet événement dans différents plugins? je sais pas si je suis bien clair.

            Je ne voyais pas le problème comme ça … A priori je ne vois pas l'intéret de le faire, mais tu dois avoir tes raisons (et j'aimerais bien savoir histoire de pouvoir te répondre au mieux). Si j'ai bien compris, tu voudrais que lorsque tu reçois un type d'évenement, tous les plugins ayant de type d'evenement défini dans son code soit exécuté (à priori, chaque plugin exécuterait des actions différentes) ? Ce n'est donc pas une surcharge. Il doit y avoir moyen de le faire, je regarde ça. Mais à priori, tu ne pourras pas appeler chaque module JOIN avec une méthode onjoin dans chaque plugin. Problème intéressant, je regarde (mais là j'ai rien d'urgent à faire pour le moment).

            • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

              Posté par  . Évalué à 2.

              Je pense avoir trouvé … Je teste.

              • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

                Posté par  . Évalué à 2.

                Voilà ce que j'ai pu faire ce soir :

                #main.rb
                module BOT
                
                    class Bot
                
                        @@modules=[]
                
                        def initialize(plugin_path)
                
                            Dir.foreach(plugin_path) do |d| 
                
                                if ((d != ".") and (d != "..")) then
                                    #puts "#{d}" 
                                    require "#{plugin_path}/#{d}"
                                    eval(%Q{@@modules << #{d.capitalize().sub(/.rb$/,"")}})
                                end 
                            end
                        end
                
                        def run
                            # parsing
                            # msg.command == "JOIN"
                            # exécuter les méthodes "onJOIN" des plugins.
                            onJoin()
                        end
                
                        def onJoin(*args)
                
                            @@modules.each do |m|
                                #puts("Module : #{m}")
                                s=%Q{#{m}::onJoin(*args)}
                                #puts(s)
                                eval(s) 
                            end
                        end
                    end
                
                end
                
                b=BOT::Bot.new("./plugins")
                b.run()
                
                #plugin3.rb
                
                module Plugin3
                
                    def self.onJoin(*args)
                        puts("Module Plugin3, event  onJoin detected,  arguments : #{args}")
                    end
                
                end
                
                # plugin4.rb
                
                module Plugin4
                
                    def self.onJoin(*args)
                        puts("Module Plugin4, event  onJoin detected,  arguments : #{args}")
                    end
                
                end
                

                Explications : tu mets le main dans un repertoire, et tu positionne les plugins dans un sous-répertoire plugins (ou celui que tu veux).

                Lorsque tu crées ton objet bot, tu lui passe le repertoire contenant les plugins. Il va aller alimenter le tableau @@modules=[] qui contiendra la liste de tous les plugins présents.

                Ensuite lorsque tu appeleras la méthode onJoin, elle ira appeler la méthode onJoin de chaque module référencé dans le tableau.

                On pourrait tester la pésence de la fonction onJoin pour chaque module (voir http://ruby-doc.org/core-1.9.3/Module.html#method-i-included_modules pour ruby 1.9.3)

                J'ai d'autres idées mais il est un peu tard ce soir, donc on verra plus tard si tu as besoin d'autres choses.

          • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

            Posté par  . Évalué à 2.

            Désolé mais je vais encore te faire perdre ton temps,
            je comprends pas :p

            Je te rassure, je ne perds pas mon temps, ça m'amuse ce genre de problème. En plus ça permet d'apprendre et de se dérouiller quand on fait plus de ruby depuis un moment (je suis sur Erlan en ce moment). C'était une boutade lorsque je te disais "méchant" :)

            • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

              Posté par  . Évalué à 0.

              Je viens de rentrer et de lire tes réponses, je prends le temps de bien comprendre ton code et je réponds plus tard.

              Mais juste pour clarifier ce que je souhaites faire.

              A priori je ne vois pas l'intéret de le faire, mais tu dois avoir tes raisons (et j'aimerais bien savoir histoire de pouvoir te répondre au mieux). Si j'ai bien compris, tu voudrais que lorsque tu reçois un type d'évenement, tous les plugins ayant de type d'evenement défini dans son code soit exécuté (à priori, chaque plugin exécuterait des actions différentes)

              Oui, c'est exactement çà.

              Je te donne un exemple, toujours avec ces événements « JOIN » quand un utilisateur rejoint un salon.
              Il pourrait y avoir une méthode qui envoie un message de bienvenue à l'utilisateur.
              Une autre qui donne les droits d'opérateur sur le salon à cet utilisateur.
              Pourquoi pas une autre qui prévient l'utilisateur parce qu'il utilise son compte root pour faire de l'IRC =/
              ceux sont des exemples mais il y a plein d'autres possibilités plus ou moins utiles rien que sur cet événement « JOIN ».

              Donc faut séparer toutes ces actions dans divers plugins selon ce que l'on veut utiliser comme fonctionnalités,
              je peux pas tout regrouper dans la même méthode.

              Merci beaucoup pour ton aide! à plus tard =)

              • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

                Posté par  . Évalué à 0.

                Je me suis inspiré de ce que tu m'as proposé avec les modules.
                Je sais pas si çà apporte grand chose en fait, j'ai repris le même principe que dans mon premier exemple pour accéder aux méthodes.

                # main.rb
                module IRC
                  module BasePlugin
                    # chaque classe qui inclut ce module
                    # est automatiquement instanciée et stockée
                    # dans Bot::plugins.
                    def self.included(pl)
                      Bot::plugins << pl.new
                    end
                
                    def exists?(msg)
                      # vérifie si une méthode on<EVENT> existe
                      # et l'exécute.
                      meth = "on" + msg.command
                      send(meth, msg) if respond_to?(meth)
                    end
                  end
                
                  class Bot
                    Message = Struct.new(:command)
                    @@plugins = []
                
                    def self.plugins
                      @@plugins
                    end
                
                    def initialize
                      Dir["./plugins/*.rb"].each { |pl| require pl }
                    end
                
                    def run(event)
                      msg = Message.new(event)
                      @@plugins.each do |pl|
                        pl.exists?(msg)
                      end
                    end
                  end
                end
                
                IRC::Bot.new.run("JOIN")
                IRC::Bot.new.run("PART")
                # plugin1.rb
                class Plugin1
                  include IRC::BasePlugin
                
                  def onJOIN(msg)
                    puts msg.command
                  end
                end

                En fait j'ai fait quasiment la même chose, au lieu d'hériter d'une classe, j'inclus un module.
                Je pense que je vais utiliser une classe toute simple comme base pour les plugins, çà me parait plus simple.

                • [^] # Re: Pour ma part, je ne passerais pas par des classes mais par des mixin

                  Posté par  . Évalué à 2. Dernière modification le 19 août 2013 à 13:05.

                  Ca peut marcher.

                  Par contre j'utiliserais method_missing à la place du bloc ci-dessous :

                  def exists?(msg)
                  # vérifie si une méthode on<EVENT> existe
                  # et l'exécute.
                  meth = "on" + msg.command
                  send(meth, msg) if respond_to?(meth)
                  end

                  dans l'objet Plugin, tu définis la méthode method_missing qui intercepte tous les appels à on_ et qui ne fait rien si l'event est défini.
                  Par contre s'il s'agit d'un appel à une autre méthode => appel à la méthode method_missing du parent.

                  Sinon, le fait qu'il n'y ait pas trop de différence entre ce que tu as fait au début et ce que j'ai proposé vient du fait que je n'avais pas compris le besoin initial.

  • # Hors ~~service~~ sujet

    Posté par  . Évalué à 2. Dernière modification le 19 août 2013 à 00:52.

    çà

    Ça c'est de la faute d'orthographe ! Je te fais cette remarque car tu me sembles avoir une orthographe tout à fait correcte mais celle-ci tu la réitères dans tes autres commentaires, ça pique les yeux !

    Notons que çà existe mais ce n'est pas la même chose…

    • [^] # Re: Hors ~~service~~ sujet

      Posté par  . Évalué à 0.

      En effet, j'ai cette mauvais habitude, je ferai attention dorénavant.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.