├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── config ├── global.yml └── seasons │ └── testing │ ├── database.yml │ ├── leaves.yml │ ├── season.yml │ └── stems.yml ├── doc └── .gitignore ├── leaves ├── administrator │ ├── Gemfile │ ├── README.md │ ├── controller.rb │ ├── data │ │ └── .gitignore │ ├── helpers │ │ └── .gitkeep │ ├── lib │ │ └── .gitkeep │ ├── models │ │ └── .gitkeep │ ├── tasks │ │ └── .gitkeep │ └── views │ │ ├── autumn.txt.erb │ │ └── reload.txt.erb ├── insulter │ ├── Gemfile │ ├── README.md │ ├── controller.rb │ ├── data │ │ └── .gitignore │ ├── helpers │ │ └── .gitkeep │ ├── models │ │ └── .gitkeep │ ├── tasks │ │ └── .gitkeep │ └── views │ │ ├── about.txt.erb │ │ ├── help.txt.erb │ │ └── insult.txt.erb └── scorekeeper │ ├── Gemfile │ ├── README.md │ ├── config.yml │ ├── controller.rb │ ├── data │ └── .gitignore │ ├── helpers │ └── general.rb │ ├── models │ ├── channel.rb │ ├── person.rb │ ├── pseudonym.rb │ └── score.rb │ ├── tasks │ └── stats.rake │ └── views │ ├── about.txt.erb │ ├── change.txt.erb │ ├── history.txt.erb │ ├── points.txt.erb │ └── usage.txt.erb ├── libs ├── authentication.rb ├── autumn.rb ├── channel_leaf.rb ├── coder.rb ├── console_boot.rb ├── ctcp.rb ├── daemon.rb ├── datamapper_hacks.rb ├── foliater.rb ├── formatting.rb ├── generator.rb ├── genesis.rb ├── inheritable_attributes.rb ├── leaf.rb ├── log_facade.rb ├── misc.rb ├── script.rb ├── speciator.rb ├── stem.rb └── stem_facade.rb ├── log └── .gitignore ├── resources └── daemons │ ├── Anothernet.yml │ ├── AustHex.yml │ ├── Bahamut.yml │ ├── Dancer.yml │ ├── GameSurge.yml │ ├── IRCnet.yml │ ├── Ithildin.yml │ ├── KineIRCd.yml │ ├── PTlink.yml │ ├── QuakeNet.yml │ ├── RFC1459.yml │ ├── RFC2811.yml │ ├── RFC2812.yml │ ├── RatBox.yml │ ├── Ultimate.yml │ ├── Undernet.yml │ ├── Unreal.yml │ ├── _Other.yml │ ├── aircd.yml │ ├── bdq-ircd.yml │ ├── hybrid.yml │ ├── hyperion.yml │ ├── ircu.yml │ └── tr-ircd.yml ├── script ├── console ├── daemon ├── destroy ├── generate └── server ├── shared └── .gitkeep └── tmp └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .yardoc 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | autumn 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | require 'pathname' 3 | 4 | gem 'daemons' 5 | gem 'anise' 6 | gem 'rake' 7 | 8 | group :pre_config do 9 | gem 'i18n' 10 | gem 'activesupport', require: 'active_support/core_ext/string' 11 | gem 'facets' 12 | end 13 | 14 | group :documentation do 15 | gem 'redcarpet' 16 | gem 'yard' 17 | end 18 | 19 | # Only loaded if a database.yml file exists for a season 20 | group :datamapper do 21 | gem 'dm-sqlite-adapter', '< 1.1.0' # Change this to whatever adapter you need for your database 22 | gem 'dm-core', '< 1.1.0' 23 | gem 'dm-migrations', '< 1.1.0' 24 | end 25 | 26 | # load each leaf's gem requirements in its own group 27 | files = Pathname.new(__FILE__).dirname.join('leaves', '*', 'Gemfile') 28 | Pathname.glob(files).each do |gemfile| 29 | eval File.read(gemfile) 30 | end 31 | 32 | # Season-specific gem requirements: 33 | # 34 | # group :season_name do 35 | # gem 'my-gem' 36 | # end 37 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (5.1.4) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | addressable (2.5.2) 10 | public_suffix (>= 2.0.2, < 4.0) 11 | anise (0.7.0) 12 | chronic (0.10.2) 13 | concurrent-ruby (1.0.5) 14 | daemons (1.2.5) 15 | data_objects (0.10.17) 16 | addressable (~> 2.1) 17 | dm-ar-finders (1.0.2) 18 | dm-core (~> 1.0.2) 19 | dm-core (1.0.2) 20 | addressable (~> 2.2) 21 | extlib (~> 0.9.15) 22 | dm-do-adapter (1.0.2) 23 | data_objects (~> 0.10.2) 24 | dm-core (~> 1.0.2) 25 | dm-migrations (1.0.2) 26 | dm-core (~> 1.0.2) 27 | dm-sqlite-adapter (1.0.2) 28 | dm-do-adapter (~> 1.0.2) 29 | do_sqlite3 (~> 0.10.2) 30 | dm-timestamps (1.0.2) 31 | dm-core (~> 1.0.2) 32 | dm-validations (1.0.2) 33 | dm-core (~> 1.0.2) 34 | do_sqlite3 (0.10.17) 35 | data_objects (= 0.10.17) 36 | extlib (0.9.16) 37 | facets (3.1.0) 38 | i18n (0.9.1) 39 | concurrent-ruby (~> 1.0) 40 | minitest (5.10.3) 41 | public_suffix (3.0.1) 42 | rake (12.3.0) 43 | redcarpet (3.4.0) 44 | thread_safe (0.3.6) 45 | tzinfo (1.2.4) 46 | thread_safe (~> 0.1) 47 | yard (0.9.12) 48 | 49 | PLATFORMS 50 | ruby 51 | 52 | DEPENDENCIES 53 | activesupport 54 | anise 55 | chronic 56 | daemons 57 | dm-ar-finders 58 | dm-core (< 1.1.0) 59 | dm-migrations (< 1.1.0) 60 | dm-sqlite-adapter (< 1.1.0) 61 | dm-timestamps 62 | dm-validations 63 | facets 64 | i18n 65 | rake 66 | redcarpet 67 | yard 68 | 69 | BUNDLED WITH 70 | 1.16.0 71 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.require :pre_config, :default, :documentation 3 | 4 | require 'rake' 5 | require 'pathname' 6 | require 'facets/pathname' 7 | 8 | $: << Dir.getwd 9 | require 'libs/autumn' 10 | $: << Autumn::Config.root.to_s unless Autumn::Config.root.to_s == Dir.getwd 11 | require 'libs/genesis' 12 | 13 | task :default do 14 | puts 'Type "rake --tasks" to see a list of tasks you can perform.' 15 | end 16 | 17 | # Load the Autumn environment. 18 | task :environment do 19 | @genesis = Autumn::Genesis.new 20 | @genesis.load_global_settings 21 | @genesis.load_season_settings 22 | end 23 | 24 | task :boot do 25 | @genesis = Autumn::Genesis.new 26 | @genesis.boot! false 27 | end 28 | 29 | namespace :app do 30 | desc "Launch the Autumn daemon" 31 | task :start do 32 | system 'script/daemon', 'start' 33 | end 34 | 35 | desc "Stop the Autumn daemon" 36 | task :stop do 37 | system 'script/daemon', 'stop' 38 | end 39 | 40 | desc "Restart the Autumn daemon" 41 | task :restart do 42 | system 'script/daemon', 'restart' 43 | end 44 | 45 | desc "Start Autumn but not as a daemon (stay on top)" 46 | task :run do 47 | system 'script/daemon', 'run' 48 | end 49 | 50 | desc "Force the daemon to a stopped state (clears PID files)" 51 | task :zap do 52 | system 'script/daemon', 'zap' 53 | end 54 | end 55 | 56 | namespace :log do 57 | desc "Remove all log files" 58 | task :clear do 59 | system 'rm', '-vf', 'tmp/*.log', 'tmp/*.output', 'log/*.log*' 60 | end 61 | 62 | desc "Print all error messages in the log files" 63 | task :errors do 64 | season_log = Pathname.new('log').join(@genesis.config.global(:season), 'log') 65 | system_log = Pathname.new('tmp').join('autumn.log') 66 | if season_log.exist? 67 | puts "==== ERROR-LEVEL LOG MESSAGES ====" 68 | File.open(season_log, 'r') do |log| 69 | puts log.grep(/^[EF],/) 70 | end 71 | end 72 | if system_log.exist? 73 | puts "==== UNCAUGHT EXCEPTIONS ====" 74 | File.open(system_log, 'r') do |log| 75 | puts log.grep(/^[EF],/) 76 | end 77 | end 78 | end 79 | end 80 | 81 | def local_db?(db) 82 | db.host.nil? || db.host == 'localhost' 83 | end 84 | 85 | namespace :db do 86 | desc "Recreate database tables according to the model objects" 87 | task migrate: :boot do 88 | dname = ENV['DB'] 89 | raise "Usage: DB=[Database config name] rake db:migrate" unless dname 90 | raise "Unknown database config #{dname}" unless repository(dname.to_sym) 91 | puts "Migrating the #{dname} database..." 92 | # Find models that have definitions for the selected database and migrate them 93 | repository(dname.to_sym) do 94 | repository(dname.to_sym).models.each { |mod| mod.auto_migrate! dname.to_sym } 95 | end 96 | end 97 | desc "Nondestructively update database tables according to the model objects" 98 | task upgrade: :boot do 99 | dname = ENV['DB'] 100 | raise "Usage: DB=[Database config name] rake db:upgrade" unless dname 101 | raise "Unknown database config #{dname}" unless repository(dname.to_sym) 102 | puts "Upgrading the #{dname} database..." 103 | # Find models that have definitions for the selected database and upgrade them 104 | repository(dname.to_sym) do 105 | repository(dname.to_sym).models.each { |mod| mod.auto_upgrade! dname.to_sym } 106 | end 107 | end 108 | end 109 | 110 | # bring sexy back (sexy == tables) 111 | module YARD::Templates::Helpers::HtmlHelper 112 | def html_markup_markdown(text) 113 | markup_class(:markdown).new(text, :gh_blockcode, :fenced_code, :autolink, :tables).to_html 114 | end 115 | end 116 | 117 | namespace :doc do 118 | desc "Generate API documentation for Autumn" 119 | YARD::Rake::YardocTask.new(:api) do |doc| 120 | api_doc = Pathname.new('doc').join('api') 121 | FileUtils.mkdir_p api_doc unless api_doc.directory? 122 | 123 | doc.options << '-m' << 'markdown' << '-M' << 'redcarpet' 124 | doc.options << '--protected' << '--no-private' 125 | doc.options << '-r' << 'README.md' 126 | doc.options << '-o' << api_doc.to_s 127 | doc.options << '--title' << "Autumn API Documentation" 128 | 129 | doc.files = %w( libs/**/*.rb README.md ) 130 | end 131 | 132 | leaf_names_and_dirs = Pathname.new('leaves').glob('*').inject({}) do |hsh, path| 133 | leaf_dir = path.realpath 134 | leaf_name = leaf_dir.basename.to_s.camelcase(:upper) 135 | hsh[leaf_name] = leaf_dir 136 | hsh 137 | end 138 | 139 | leaf_names_and_dirs.each do |name, path| 140 | desc "Generate documentation for the #{name} leaf" 141 | YARD::Rake::YardocTask.new(name.snakecase.to_sym) do |doc| 142 | output_dir = path.join('..', '..', 'doc', 'leaves', name.snakecase) 143 | FileUtils.mkdir_p output_dir unless output_dir.directory? 144 | 145 | doc.options << '-m' << 'markdown' << '-M' << 'redcarpet' 146 | doc.options << '--protected' << '--no-private' 147 | doc.options << '-r' << path.join('README.md').to_s 148 | doc.options << '-o' << output_dir.realpath.to_s 149 | doc.options << '--title' << "#{name} Documentation" 150 | 151 | doc.files = [ 152 | path.join('controller.rb'), 153 | path.join('helpers', '**', '*.rb'), 154 | path.join('models', '**', '*.rb'), 155 | path.join('README.md') 156 | ].map(&:to_s) 157 | end 158 | end 159 | 160 | desc "Generate documentation for all leaves" 161 | task leaves: leaf_names_and_dirs.map { |(name, _)| name.snakecase.to_sym } 162 | 163 | desc "Generate all documentation" 164 | task all: [:api, :leaves] 165 | 166 | desc "Remove all documentation" 167 | task :clear do 168 | api_doc = Pathname.new('doc').join('api') 169 | leaves_doc = Pathname.new('doc').join('leaves') 170 | FileUtils.remove_dir api_doc if api_doc.directory? 171 | FileUtils.remove_dir leaves_doc if leaves_doc.directory? 172 | end 173 | end 174 | 175 | # Load any custom Rake tasks in the bot's tasks directory. 176 | Pathname.new('leaves').glob('*').each do |leaf| 177 | leaf_name = leaf.basename('.rb').to_s.downcase 178 | namespace leaf_name.to_sym do # Tasks are placed in a namespace named after the leaf 179 | Pathname.glob(leaf.join('tasks', '**', '*.rake')).sort.each do |task| 180 | load task 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /config/global.yml: -------------------------------------------------------------------------------- 1 | --- 2 | season: testing 3 | -------------------------------------------------------------------------------- /config/seasons/testing/database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Scorekeeper: sqlite:leaves/scorekeeper/database.db 3 | -------------------------------------------------------------------------------- /config/seasons/testing/leaves.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Administrator: 3 | authentication: 4 | type: op 5 | class: Administrator 6 | Scorekeeper: 7 | class: Scorekeeper 8 | Insulter: 9 | class: Insulter 10 | -------------------------------------------------------------------------------- /config/seasons/testing/season.yml: -------------------------------------------------------------------------------- 1 | --- 2 | logging: debug 3 | -------------------------------------------------------------------------------- /config/seasons/testing/stems.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Example: 3 | nick: MyIRCBot 4 | leaves: 5 | - Administrator 6 | - Scorekeeper 7 | - Insulter 8 | rejoin: true 9 | channel: "#yourchannel" 10 | server: irc.yourircserver.com 11 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /leaves/administrator/Gemfile: -------------------------------------------------------------------------------- 1 | group :administrator do 2 | # No gem requirements 3 | end 4 | -------------------------------------------------------------------------------- /leaves/administrator/README.md: -------------------------------------------------------------------------------- 1 | Administrator: An Autumn Leaf 2 | ============================= 3 | 4 | **Version 1.0 (Jul 8, 2008)** 5 | 6 | | **Author** | Tim Morgan (autumn@timothymorgan.info) | 7 | | **Copyright** | Copyright (c)2008 Tim Morgan | 8 | | **License** | Distributed under the same terms as Ruby. | 9 | 10 | This leaf allows the owner of the IRC bot to perform administrative commands on 11 | the bot, such as quitting or reloading it. In order to protect such commands 12 | from abuse, this bot uses the `Authenticator` module. The Authenticator method 13 | that is used is specified by the `authentication` option. For more information, 14 | see the **Authentication** section of the Autumn README. 15 | 16 | Usage 17 | ----- 18 | 19 | | `!autumn` | Displays information about the version of Autumn that is running this leaf (unprotected command). | 20 | | `!quit` | Terminates the stem on which the message was received. | 21 | | `!reload` | Reloads all source files of all leaves, and all their view files. | 22 | -------------------------------------------------------------------------------- /leaves/administrator/controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for the Administrator leaf. 2 | 3 | class Controller < Autumn::Leaf 4 | 5 | # Typing this command reloads all source code for all leaves and support 6 | # files, allowing you to make "on-the-fly" changes without restarting the 7 | # process. It does this by reloading the source files defining the classes. 8 | # 9 | # If you supply the configuration name of a leaf, only that leaf is reloaded. 10 | # 11 | # This command does not reload the YAML configuration files, only the source 12 | # code. 13 | 14 | def reload_command(_, _, _, msg) 15 | var leaves: Hash.new 16 | if msg then 17 | if Foliater.instance.leaves.include?(msg) 18 | begin 19 | Foliater.instance.hot_reload Foliater.instance.leaves[msg] 20 | rescue 21 | logger.error "Error when reloading #{msg}:" 22 | logger.error $! 23 | var(:leaves)[msg] = $!.to_s 24 | else 25 | var(:leaves)[msg] = false 26 | end 27 | logger.info "#{msg}: Reloaded" 28 | else 29 | var not_found: msg 30 | end 31 | else 32 | Foliater.instance.leaves.each do |name, leaf| 33 | begin 34 | Foliater.instance.hot_reload leaf 35 | rescue 36 | logger.error "Error when reloading #{name}:" 37 | logger.error $! 38 | var(:leaves)[name] = $!.to_s 39 | else 40 | var(:leaves)[name] = false 41 | end 42 | logger.info "#{name}: Reloaded" 43 | end 44 | end 45 | end 46 | ann :reload_command, protected: true 47 | 48 | # Typing this command will cause the Stem to exit. 49 | 50 | def quit_command(stem, _, _, _) 51 | stem.quit 52 | end 53 | ann :quit_command, protected: true 54 | 55 | # Typing this command will display information about the version of Autumn 56 | # that is running this leaf. 57 | 58 | def autumn_command(_, _, _, _) 59 | var version: Autumn::Config.version 60 | end 61 | 62 | # Suppress the "!commands" command; don't want to publicize the administrative 63 | # features. 64 | 65 | def commands_command(_, _, _, _) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /leaves/administrator/data/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /leaves/administrator/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RISCfuture/autumn/2d72bb5e5c5da9a1f23568908ef4b0aece982767/leaves/administrator/helpers/.gitkeep -------------------------------------------------------------------------------- /leaves/administrator/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RISCfuture/autumn/2d72bb5e5c5da9a1f23568908ef4b0aece982767/leaves/administrator/lib/.gitkeep -------------------------------------------------------------------------------- /leaves/administrator/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RISCfuture/autumn/2d72bb5e5c5da9a1f23568908ef4b0aece982767/leaves/administrator/models/.gitkeep -------------------------------------------------------------------------------- /leaves/administrator/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RISCfuture/autumn/2d72bb5e5c5da9a1f23568908ef4b0aece982767/leaves/administrator/tasks/.gitkeep -------------------------------------------------------------------------------- /leaves/administrator/views/autumn.txt.erb: -------------------------------------------------------------------------------- 1 | Autumn version <%= var :version %>, an IRC bot framework for Ruby (http://github.com/RISCfuture/autumn). 2 | -------------------------------------------------------------------------------- /leaves/administrator/views/reload.txt.erb: -------------------------------------------------------------------------------- 1 | <% if var(:not_found) %> 2 | There is no leaf named <%= var :not_found %>. 3 | <% else %> 4 | <% var(:leaves).each do |leaf, error| %> 5 | <% if error %> 6 | <%= leaf %> couldn't be reloaded: <%= error %> 7 | <% else %> 8 | <%= leaf %> was reloaded successfully. 9 | <% end %> 10 | <% end %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /leaves/insulter/Gemfile: -------------------------------------------------------------------------------- 1 | group :insulter do 2 | # No gem requirements 3 | end 4 | -------------------------------------------------------------------------------- /leaves/insulter/README.md: -------------------------------------------------------------------------------- 1 | Insulter: An Autumn Leaf 2 | ======================== 3 | 4 | **Version 1.0 (Jul 4, 2008)** 5 | 6 | | **Author** | Tim Morgan (autumn@timothymorgan.info) | 7 | | **Copyright** | Copyright (c)2008 Tim Morgan | 8 | | **License** | Distributed under the same terms as Ruby. Portions of this content are copyright (c)1996 Main Strike Telecommunications, Inc. | 9 | 10 | A simple Autumn Leaf that generates faux-Shakespearean insults. This leaf 11 | required no database and no additional gems beyond that of a normal Autumn 12 | install. 13 | 14 | Usage 15 | ----- 16 | 17 | | `!insult [name]` | The bot lets fly a barbarous insult against [name], whose very spirit will thenceforth be crushed into a negligible pulp. | 18 | -------------------------------------------------------------------------------- /leaves/insulter/controller.rb: -------------------------------------------------------------------------------- 1 | # Controller and model for the Insulter leaf; maintains the list of insult 2 | # substrings and chooses from them randomly. 3 | 4 | class Controller < Autumn::Leaf 5 | 6 | # Insults the unfortunate argument of this command. 7 | 8 | def insult_command(_, _, _, msg) 9 | msg.nil? ? render(:help) : insult(msg.capitalize) 10 | end 11 | 12 | # Displays information about the leaf. 13 | 14 | def about_command(_, _, _, _) 15 | end 16 | 17 | private 18 | 19 | # @private 20 | ADJECTIVES = [ 21 | 'an artless', 'a bawdy', 'a beslubbering', 'a bootless', 'a churlish', 22 | 'a clouted', 'a cockered', 'a craven', 'a currish', 'a dankish', 23 | 'a dissembling', 'a droning', 'an errant', 'a fawning', 'a fobbing', 24 | 'a frothy', 'a froward', 'a gleeking', 'a goatish', 'a gorbellied', 25 | 'an impertinent', 'an infectious', 'a jarring', 'a loggerheaded', 26 | 'a lumpish', 'a mammering', 'a mangled', 'a mewling', 'a paunchy', 27 | 'a pribbling', 'a puking', 'a puny', 'a qualling', 'a rank', 'a reeky', 28 | 'a roguish', 'a ruttish', 'a saucy', 'a spleeny', 'a spongy', 'a surly', 29 | 'a tottering', 'an unmuzzled', 'a vain', 'a venomed', 'a villainous', 30 | 'a warped', 'a wayward', 'a weedy', 'a yeasty' 31 | ] 32 | 33 | # @private 34 | PARTICIPLES = %w(base-court bat-fowling beef-witted beetle-headed boil-brained 35 | clapper-clawed clay-brained common-kissing crook-pated 36 | dismal-dreaming dizzy-eyed doghearted dread-bolted 37 | earth-vexing elf-skinned fat-kidneyed fen-sucked flap-mouthed 38 | fly-bitten folly-fallen fool-born full-gorged guts-griping 39 | half-faced hasty-witted hedge-born hell-hated idle-headed 40 | ill-breeding ill-nurtured knotty-pated milk-livered 41 | motley-minded onion-eyed plume-plucked pottle-deep pox-marked 42 | reeling-ripe rough-hewn rude-growing rump-fed shard-borne 43 | sheep-biting spur-galled swag-bellied tardy-gaited 44 | tickle-brained toad-spotted urchin-snouted weather-bitten) 45 | 46 | # @private 47 | NOUNS = %w(apple-john baggage barnacle bladder boar-pig bugbear bum-bailey 48 | canker-blossom clack-dish clotpole codpiece coxcomb death-token 49 | dewberry flap-dragon flax-wench flirt-gill foot-licker fustilarian 50 | giglet gudgeon haggard harpy hedge-pig horn-beast hugger-mugger 51 | joithead lewdster lout maggot-pie malt-worm mammet measle minnow 52 | miscreant moldwarp mumble-news nut-hook pigeon-egg pignut pumpion 53 | puttock ratsbane scut skainsmate strumpet varlet vassal wagtail 54 | whey-face) 55 | 56 | def insult(victim) 57 | var adjective: ADJECTIVES.sample 58 | var participle: PARTICIPLES.sample 59 | var noun: NOUNS.sample 60 | var victim: victim 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /leaves/insulter/data/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /leaves/insulter/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RISCfuture/autumn/2d72bb5e5c5da9a1f23568908ef4b0aece982767/leaves/insulter/helpers/.gitkeep -------------------------------------------------------------------------------- /leaves/insulter/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RISCfuture/autumn/2d72bb5e5c5da9a1f23568908ef4b0aece982767/leaves/insulter/models/.gitkeep -------------------------------------------------------------------------------- /leaves/insulter/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RISCfuture/autumn/2d72bb5e5c5da9a1f23568908ef4b0aece982767/leaves/insulter/tasks/.gitkeep -------------------------------------------------------------------------------- /leaves/insulter/views/about.txt.erb: -------------------------------------------------------------------------------- 1 | Insulter version 1.0 (7-4-08) by Tim Morgan: An Autumn Leaf. Insults copyright (c)1996 Main Strike Telecommunications, Inc. 2 | -------------------------------------------------------------------------------- /leaves/insulter/views/help.txt.erb: -------------------------------------------------------------------------------- 1 | Type "!insult Ted" to crush Ted's spirit with a devastating insult. 2 | -------------------------------------------------------------------------------- /leaves/insulter/views/insult.txt.erb: -------------------------------------------------------------------------------- 1 | <%= var :victim %>, thou art <%= var :adjective %> <%= var :participle %> <%= var :noun %>! 2 | -------------------------------------------------------------------------------- /leaves/scorekeeper/Gemfile: -------------------------------------------------------------------------------- 1 | # Bot-specific gem requirements 2 | group :scorekeeper do 3 | gem 'chronic' 4 | end 5 | 6 | # Additions to the DataMapper requirements 7 | group :datamapper do 8 | gem 'dm-ar-finders' 9 | gem 'dm-validations' 10 | gem 'dm-timestamps' 11 | end 12 | -------------------------------------------------------------------------------- /leaves/scorekeeper/README.md: -------------------------------------------------------------------------------- 1 | Scorekeeper: An Autumn Leaf 2 | =========================== 3 | 4 | **Version 3.0 (Jul 4, 2008)** 5 | 6 | | **Author** | Tim Morgan (autumn@timothymorgan.info) | 7 | | **Copyright** | Copyright (c)2007-2008 Tim Morgan | 8 | | **License** | Distributed under the same terms as Ruby. | 9 | 10 | An Autumn Leaf used for an in-channel scorekeeping system. This can operate both 11 | as an open or closed score system. (In the former, new members are automatically 12 | added when they receive points; in the latter, only authorized members can give 13 | and receive points.) 14 | 15 | Scorekeeper is a database-backed leaf. It requires 16 | [the DataMapper gem](http://www.datamapper.org) in order to run. The database 17 | stores channels and their members, and each member's point history. 18 | 19 | Scorekeeper supports pseudonyms. Entries in the `pseudonyms` table can be used 20 | to help ensure that the correct person's points are changed even when the sender 21 | uses a nickname or abbreviation. 22 | 23 | Scorekeeper takes one custom configuration option, `scoring`, which can be 24 | either "open" or "closed". A closed system only allows a specified set of users 25 | to receive and give points. An open system allows anyone to award points to 26 | anyone. 27 | 28 | Usage 29 | ----- 30 | 31 | | `!points [name]` | Get a person's score. | 32 | | `!points [name] [+|-][number] [reason]` | Change a person's score (you must have a "+" or a "-"). A reason is optional. | 33 | | `!points [name] history` | Return some recent history of that person's score. | 34 | | `!points [name] history [time period]` | Selects history from a time period. | 35 | | `!points [name] history [sender]` | Selects point changes from a sender. | 36 | -------------------------------------------------------------------------------- /leaves/scorekeeper/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | scoring: open 3 | -------------------------------------------------------------------------------- /leaves/scorekeeper/controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for the Scorekeeper leaf. This class contains only the methods 2 | # directly relating to IRC. Other methods are stored in the helper and model 3 | # classes. 4 | 5 | class Controller < Autumn::Leaf 6 | 7 | # Displays an about message. 8 | 9 | def about_command(_, _, _, _) 10 | end 11 | 12 | # Displays the current point totals, or modifies someone's score, depending on 13 | # the message provided with the command. 14 | 15 | def points_command(stem, sender, reply_to, msg) 16 | if msg.blank? 17 | var totals: totals(stem, reply_to) 18 | elsif msg =~ /^(#{stem.nick_regex})\s+history\s*(.*)$/ 19 | parse_history stem, reply_to, $1, $2 20 | render :history 21 | elsif msg =~ /^(#{nick_regex})\s+([\+\-]\d+)\s*(.*)$/ 22 | parse_change stem, reply_to, sender, $1, $2.to_i, $3 23 | render :change 24 | else 25 | render :usage 26 | end 27 | end 28 | 29 | private 30 | 31 | def points(stem, channel) 32 | chan = Channel.find_or_create(server: server_identifier(stem), name: channel) 33 | scores = chan.scores.all 34 | scores.inject(Hash.new(0)) { |hsh, score| hsh[score.receiver.name] += score.change; hsh } 35 | end 36 | 37 | def totals(stem, channel) 38 | points(stem, channel).sort { |a, b| b.last <=> a.last } 39 | end 40 | 41 | def parse_change(stem, channel, sender, victim, delta, note) 42 | giver = find_person(stem, sender[:nick]) 43 | if giver.nil? && options[:scoring] == 'open' 44 | giver ||= Person.create(server: server_identifier(stem), name: sender[:nick]) 45 | end 46 | 47 | receiver = find_person(stem, victim) 48 | if receiver.nil? && options[:scoring] == 'open' 49 | receiver ||= Person.create(server: server_identifier(stem), name: find_in_channel(stem, channel, victim)) 50 | end 51 | 52 | unless authorized?(giver, receiver) 53 | var unauthorized: true 54 | var receiver: receiver.nil? ? victim : receiver.name 55 | return 56 | end 57 | 58 | change_points stem, channel, giver, receiver, delta, note 59 | var giver: giver 60 | var receiver: receiver 61 | var delta: delta 62 | end 63 | 64 | def parse_history(stem, channel, subject, argument) 65 | date = argument.empty? ? nil : parse_date(argument) 66 | 67 | chan = Channel.named(channel).first 68 | person = find_person(stem, subject) 69 | if person.nil? 70 | var person: subject 71 | var no_history: true 72 | return 73 | end 74 | 75 | giver = nil 76 | if date 77 | start, stop = find_range(date) 78 | scores = chan.scores.given_to(person).between(start, stop).newest_first.all(limit: 5) 79 | elsif argument.empty? 80 | scores = chan.scores.given_to(person).newest_first.all(limit: 5) 81 | else 82 | giver = find_person(stem, argument) 83 | if giver.nil? 84 | var giver: argument 85 | var receiver: person 86 | var no_giver_history: true 87 | return 88 | end 89 | scores = chan.scores.given_by(giver).given_to(person).newest_first.all(limit: 5) 90 | end 91 | var receiver: person 92 | var giver: giver 93 | var scores: scores 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /leaves/scorekeeper/data/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /leaves/scorekeeper/helpers/general.rb: -------------------------------------------------------------------------------- 1 | # Utility methods used by Scorekeeper. 2 | 3 | module GeneralHelper 4 | private 5 | 6 | def parse_date(str) 7 | date = nil 8 | begin 9 | date = Chronic.parse(str, context: :past, guess: false) 10 | rescue NameError 11 | begin 12 | date = Date.parse(str) 13 | rescue ArgumentError 14 | # ignored 15 | end 16 | end 17 | return date 18 | end 19 | 20 | def find_range(date) 21 | if date.kind_of? Range 22 | start = date.first 23 | stop = date.last 24 | elsif date.kind_of? Time 25 | start = date.to_date 26 | stop = date.to_date + 1 27 | else 28 | start = date 29 | stop = date + 1 30 | end 31 | return start, stop 32 | end 33 | 34 | def find_person(stem, nick) 35 | Person.all(server: server_identifier(stem)).each do |person| 36 | return person if person.name.downcase == normalize(nick) || person.pseudonyms.collect { |pn| pn.name.downcase }.include?(normalize(nick)) 37 | end 38 | return nil 39 | end 40 | 41 | def find_in_channel(stem, channel, victim) 42 | stem.channel_members[channel].each do |name, _| 43 | return normalize(name, false) if normalize(name) == normalize(victim) 44 | end 45 | return victim 46 | end 47 | 48 | def normalize(nick, dc=true) 49 | dc ? nick.downcase.split(/\|/)[0] : nick.split(/\|/)[0] 50 | end 51 | 52 | def authorized?(giver, receiver) 53 | giver && receiver && giver.authorized? && giver.name != receiver.name 54 | end 55 | 56 | def change_points(stem, channel, giver, receiver, delta, note=nil) 57 | return if delta.zero? 58 | chan = Channel.find_or_create server: server_identifier(stem), name: channel 59 | chan.scores.create giver: giver, receiver: receiver, change: delta, note: note.force_encoding('UTF-8') 60 | end 61 | 62 | def server_identifier(stem) 63 | "#{stem.server}:#{stem.port}" 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /leaves/scorekeeper/models/channel.rb: -------------------------------------------------------------------------------- 1 | # An IRC server and channel. The server property is of the form 2 | # "[address]:[port]". 3 | # 4 | # Associations 5 | # ------------ 6 | # 7 | # | `scores` | The {Score Scores} awarded on this channel. | 8 | # 9 | # Properties 10 | # ---------- 11 | # 12 | # | `server` | The address of the server this channel is on. | 13 | # | `name` | The name of this channel, including the "#". | 14 | 15 | class Channel 16 | include DataMapper::Resource 17 | 18 | property :id, Serial 19 | property :server, String, key: true 20 | property :name, String, key: true 21 | 22 | has n, :scores 23 | 24 | # Finds a channel by name. 25 | # 26 | # @param [String] name A channel name. 27 | # @return [Array] The channels with that name. 28 | 29 | def self.named(name) 30 | all(name: name) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /leaves/scorekeeper/models/person.rb: -------------------------------------------------------------------------------- 1 | # An IRC member who can give or receive points. 2 | # 3 | # Associations 4 | # ------------ 5 | # 6 | # | `scores` | The {Score Scores} given to this Person. | 7 | # | `scores_awarded` | The {Score Scores} awarded by this Person to others. | 8 | # | `pseudonyms` | The {Pseudonym Pseudonyms} this Person goes by on IRC. | 9 | # 10 | # Associations 11 | # ------------ 12 | # 13 | # | `server` | The address of the IRC server on which this nick was seen. | 14 | # | `name` | The nick this person used. | 15 | # | `authorized` | If `true`, this Person can award scores. | 16 | 17 | class Person 18 | include DataMapper::Resource 19 | 20 | property :id, Serial 21 | property :server, String, required: true, unique_index: :server_and_name 22 | property :name, String, required: true, unique_index: :server_and_name 23 | property :authorized, Boolean, required: true, default: true 24 | 25 | has n, :scores, child_key: [:receiver_id] 26 | has n, :scores_awarded, model: 'Score', child_key: [:giver_id] 27 | has n, :pseudonyms 28 | end 29 | -------------------------------------------------------------------------------- /leaves/scorekeeper/models/pseudonym.rb: -------------------------------------------------------------------------------- 1 | # A Person's nickname. If someone changes a person's points using this nickname, 2 | # the correct {Person} instance has their points changed. 3 | # 4 | # Associations 5 | # ------------ 6 | # 7 | # | `person` | The {Person} this is a nickname for. 8 | # 9 | # Properties 10 | # ---------- 11 | # 12 | # | `name` | The nickname. | 13 | 14 | class Pseudonym 15 | include DataMapper::Resource 16 | 17 | property :id, Serial 18 | property :name, String, required: true, index: true 19 | property :person_id, Integer, required: true, index: true 20 | 21 | belongs_to :person 22 | end 23 | -------------------------------------------------------------------------------- /leaves/scorekeeper/models/score.rb: -------------------------------------------------------------------------------- 1 | # A change to a Person's score. It can be positive or negative. 2 | # 3 | # Associations 4 | # ------------ 5 | # 6 | # | `giver` | The {Person} who awarded the points. | 7 | # | `receiver` | The {Person} who received the points. | 8 | # | `channel` | The {Channel} in which the transaction took place. | 9 | # 10 | # Properties 11 | # ---------- 12 | # 13 | # | `change` | The delta value in points, positive or negative. | 14 | # | `note` | An optional note describing why the points were changed. | 15 | 16 | class Score 17 | include DataMapper::Resource 18 | 19 | property :id, Serial 20 | property :giver_id, Integer, required: true, index: :giver_and_receiver 21 | property :receiver_id, Integer, required: true, index: :giver_and_receiver 22 | property :channel_id, Integer, required: true, index: true 23 | property :change, Integer, required: true, default: 0 24 | property :note, String 25 | timestamps :created_at 26 | 27 | belongs_to :giver, model: 'Person', child_key: [:giver_id] 28 | belongs_to :receiver, model: 'Person', child_key: [:receiver_id] 29 | belongs_to :channel 30 | 31 | validates_with_method :cant_give_scores_to_self 32 | 33 | # Returns Scores given to a Person. 34 | # 35 | # @param [Person, Array] people The person or people to list Scores 36 | # for. 37 | # @return [Array] The Scores that this Person received. 38 | 39 | def self.given_to(people) 40 | people = [people] unless people.kind_of?(Enumerable) 41 | people.map! { |person| person.kind_of?(Person) ? person.id : person } 42 | all(receiver_id: people) 43 | end 44 | 45 | # Returns Scores awarded by a Person. 46 | # 47 | # @param [Person, Array] people The person or people to list Scores 48 | # for. 49 | # @return [Array] The Scores that this Person gave. 50 | 51 | def self.given_by(people) 52 | people = [people] unless people.kind_of?(Enumerable) 53 | people.map! { |person| person.kind_of?(Person) ? person.id : person } 54 | all(giver_id: people) 55 | end 56 | 57 | # Returns Scores given between two dates. 58 | # 59 | # @param [Time] start A start date. 60 | # @param [Time] stop An end date. 61 | # @return [Array] The Scores between these times. 62 | 63 | def self.between(start, stop) 64 | all(created_at: start..stop) 65 | end 66 | 67 | # @return [Array] Scores in descending order of newness. 68 | 69 | def self.newest_first 70 | all(order: [:created_at.desc]) 71 | end 72 | 73 | private 74 | 75 | def cant_give_scores_to_self 76 | if giver_id == receiver_id then 77 | [false, "You can't change your own score."] 78 | else 79 | true 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /leaves/scorekeeper/tasks/stats.rake: -------------------------------------------------------------------------------- 1 | desc "Display the current scores of every channel" 2 | task scores: :boot do 3 | Autumn::Foliater.instance.leaves.select { |_, leaf| leaf.kind_of? Scorekeeper::Controller }.each do |name, leaf| 4 | puts "Leaf #{name}" 5 | leaf.database do 6 | Scorekeeper::Channel.all.group_by { |chan| chan.server }.each do |server, channels| 7 | puts " #{server}" 8 | channels.each do |channel| 9 | scores = channel.scores 10 | vals = scores.inject(Hash.new(0)) { |hsh, score| hsh[score.receiver.name] += score.change; hsh } 11 | print_scores = vals.sort { |a, b| b.last <=> a.last }.collect { |n, p| "#{n}: #{p}" }.join(', ') 12 | puts " #{channel.name} - #{print_scores}" 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /leaves/scorekeeper/views/about.txt.erb: -------------------------------------------------------------------------------- 1 | Scorekeeper version 3.0 (7-4-08) by Tim Morgan: An Autumn Leaf. 2 | -------------------------------------------------------------------------------- /leaves/scorekeeper/views/change.txt.erb: -------------------------------------------------------------------------------- 1 | <% if var(:unauthorized) %> 2 | You can't change <%= var :receiver %>'s points. 3 | <% else %> 4 | <%= var(:giver).name %> <%= var(:delta) > 0 ? 'gave' : 'docked' %> <%= var(:receiver).name %> <%= var(:delta).abs.pluralize('point') %>. 5 | <% end %> 6 | -------------------------------------------------------------------------------- /leaves/scorekeeper/views/history.txt.erb: -------------------------------------------------------------------------------- 1 | <% if var(:no_history) %> 2 | <%= var :person %> is not a part of the scoring system. 3 | <% elsif var(:no_giver_history) %> 4 | <%= var :giver %> has not awarded any points to <%= var(:receiver).name %>. 5 | <% elsif var(:scores).empty? %> 6 | <%= var(:receiver).name %> has no points history. 7 | <% else %> 8 | <% var(:scores).each do |score| %> 9 | [<%= score.created_at.strftime '%m/%d %I:%M %p' %>] <%= score.giver.name %> <%= score.change > 0 ? 'gave' : 'docked' %> <%= score.receiver.name %> <%= score.change.abs.pluralize('point') %><% if score.note %>: <%= score.note %><% end %> 10 | <% end %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /leaves/scorekeeper/views/points.txt.erb: -------------------------------------------------------------------------------- 1 | <% if var(:totals).empty? %> 2 | No one has any points yet. 3 | <% else %> 4 | <%= var(:totals).collect { |person, score| "#{person}: #{score}" }.join(', ') %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /leaves/scorekeeper/views/usage.txt.erb: -------------------------------------------------------------------------------- 1 | Examples: "!points", "!points Sancho +5", "!points Smith history", "!points Sancho history 2/27/08", "!points Sancho history Smith" 2 | -------------------------------------------------------------------------------- /libs/authentication.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # Defines classes which each encapsulate a different strategy for 4 | # authentication. When the `authentication` option is specified (see the 5 | # {Leaf} class), the options given are used to choose the correct class within 6 | # this module to serve as the authenticator for that leaf. 7 | # 8 | # These authentication strategies are used to ensure only authorized users 9 | # have access to protected commands. Leaf authors can designate certain 10 | # commands as protected. 11 | # 12 | # Writing Your Own Authenticators 13 | # ------------------------------- 14 | # 15 | # To define your own authenticator, subclass Base as a new class in the 16 | # Authentication module. Implement the methods defined in the Base class docs, 17 | # then adjust your configuration to use your new authenticator. 18 | 19 | module Authentication 20 | 21 | # @abstract 22 | # 23 | # The basic subclass for all authenticators. If you wish to write your own 24 | # authenticator, you must subclass this class. You must at a minimum 25 | # override the {#authenticate} method. You should also override the 26 | # {#initialize} method if you need to store any options or other data for 27 | # later use. 28 | # 29 | # The authentication module will become a stem listener, so see 30 | # {Stem#add_listener} for information on other methods you can implement. 31 | 32 | class Base 33 | 34 | # @abstract 35 | # 36 | # Stores the options for this authenticator and configures it for use. 37 | # 38 | # @param [Hash] options The authenticator configuration. 39 | 40 | def initialize(options={}) 41 | raise "You can only instantiate subclasses of this class." 42 | end 43 | 44 | # @abstract 45 | # 46 | # Returns `true` if the user is authenticated, `false` if not. 47 | # 48 | # @param [Stem] stem The Stem on which the Leaf is running. 49 | # @param [String] channel The channel name. 50 | # @param [Hash] sender A sender hash (see {Stem} class documentation). 51 | # @param [Leaf] leaf The Leaf that is requesting user authentication. 52 | 53 | def authenticate(stem, channel, sender, leaf) 54 | raise "Subclasses must override the Autumn::Authentication::Base#authenticate method." 55 | end 56 | 57 | # Returns a string to be displayed to a user who is not authorized to 58 | # perform a command. Override this method to provide more specific hints 59 | # to a user on what he can do to authorize himself (e.g., "Tell me your 60 | # password"). 61 | 62 | def unauthorized 63 | "You must be an administrator for this bot to do that." 64 | end 65 | end 66 | 67 | # Authenticates users by their privilege level in the channel they ran the 68 | # command in. 69 | # 70 | # This is a quick, configuration-free way of protecting your leaf, so long 71 | # as you trust the ops in your channel. 72 | 73 | class Op < Base 74 | 75 | # Creates a new authenticator. Pass a list of allowed privileges (as 76 | # symbols) for the `privileges` option. By default this class accepts ops, 77 | # admins, and channel owners/founders as authorized. 78 | 79 | def initialize(options={}) 80 | @privileges = options[:privileges] 81 | @privileges ||= [:operator, :oper, :op, :admin, :founder, :channel_owner] 82 | end 83 | 84 | def authenticate(stem, channel, sender, leaf) 85 | # Returns true if the sender has any of the privileges listed below 86 | !(@privileges & [stem.privilege(channel, sender)].flatten).empty? 87 | end 88 | 89 | def unauthorized # :nodoc: 90 | "You must be an op to do that." 91 | end 92 | end 93 | 94 | # Authenticates by IRC nick. A list of allowed nicks is built on 95 | # initialization, and anyone with that nick is allowed to use restricted 96 | # commands. 97 | # 98 | # This is the most obvious approach to authentication, but it is hardly 99 | # secure. Anyone can change their nick to an admin's nick once that admin 100 | # logs out. 101 | 102 | class Nick < Base 103 | 104 | # Creates a new authenticator. Pass a single nick for the `nick` option or 105 | # an array of allowed nicks for the `nicks` option. If neither option is 106 | # set, an exception is raised. 107 | 108 | def initialize(options={}) 109 | @nicks = options[:nick] 110 | @nicks ||= options[:nicks] 111 | raise "You must give the nick of an administrator to use nick-based authentication." if @nicks.nil? 112 | @nicks = [@nicks] if @nicks.kind_of? String 113 | end 114 | 115 | def authenticate(stem, channel, sender, leaf) 116 | @nicks.include? sender[:nick] 117 | end 118 | end 119 | 120 | # Authenticates by the host portion of an IRC message. A hostmask is used to 121 | # match the relevant portion of the address with a whitelist of accepted 122 | # host addresses. 123 | # 124 | # This method can be a secure way of preventing unauthorized access if you 125 | # choose an appropriately narrow hostmask. However, you must configure in 126 | # advance the computers you may want to administrate your leaves from. 127 | 128 | class Hostname < Base 129 | 130 | # Creates a new authenticator. You provide a hostmask via the `hostmask` 131 | # option -- either a Regexp with one capture (that captures the portion of 132 | # the hostmask you are interested in), or a Proc, which takes a host as an 133 | # argument and returns true if the host is authorized, false if not. If 134 | # the `hostmask` option is not provided, a standard hostmask regexp will 135 | # be used. This regexp strips everything left of the first period; for the 136 | # example hostmask "wsd1.ca.widgetcom.net", it would return 137 | # "ca.widgetcom.net" to be used for comparison. 138 | # 139 | # You also provide an authorized host with the `host` option, or a list of 140 | # such hosts with the `hosts` option. If neither is given, an exception is 141 | # raised. 142 | 143 | def initialize(options={}) 144 | @hostmask = options[:hostmask] 145 | @hostmask ||= /^.+?\.(.+)$/ 146 | @hostmask = @hostmask.to_rx(false) if @hostmask.kind_of? String 147 | if @hostmask.kind_of? Regexp 148 | mask = @hostmask 149 | @hostmask = lambda do |host| 150 | (matches = host.match(mask)) ? matches[1] : nil 151 | end 152 | end 153 | 154 | @hosts = options[:host] 155 | @hosts ||= options[:hosts] 156 | raise "You must give the host address of an administrator to use nick-based authentication." unless @hosts 157 | @hosts = [@hosts] unless @hosts.kind_of? Array 158 | end 159 | 160 | def authenticate(stem, channel, sender, leaf) 161 | @hosts.include? @hostmask.call(sender[:host]) 162 | end 163 | end 164 | 165 | # Authenticates by a password provided in secret. When a user `PRIVMSG`'s 166 | # the leaf the correct password, the leaf adds that user's nick to a list of 167 | # authorized nicks. These credentials expire when the person changes his 168 | # nick, logs out, leaves the channel, etc. They also expire if a certain 169 | # amount of time passes without running any protected commands. 170 | 171 | class Password < Base 172 | # The default period of time that must occur with no use of protected 173 | # commands after which a user's credentials expire. 174 | DEFAULT_EXPIRE_TIME = 5.minutes 175 | 176 | # Creates a new authenticator. You provide a valid password with the 177 | # `password` option. If that option is not provided, an exception is 178 | # raised. You can pass a number of seconds to the `expire_time` option; 179 | # this is the amount of time that must pass with no protected commands for 180 | # a nick's authorization to expire. If the `expire_time` option is not 181 | # given, a default value of five minutes is used. 182 | 183 | def initialize(options={}) 184 | @password = options[:password] 185 | @expire_time = options[:expire_time] 186 | @expire_time ||= DEFAULT_EXPIRE_TIME 187 | raise "You must provide a password to use password-based authentication" unless @password 188 | @authorized_nicks = Hash.new { |hsh, key| hsh[key] = Set.new } 189 | @last_protected_action = Hash.new { |hsh, key| hsh[key] = Hash.new(Time.at(0)) } 190 | @an_lock = Mutex.new 191 | end 192 | 193 | # @private 194 | def irc_privmsg_event(stem, sender, arguments) 195 | if arguments[:recipient] && arguments[:message] == @password 196 | @an_lock.synchronize do 197 | @authorized_nicks[stem] << sender[:nick] 198 | @last_protected_action[stem][sender[:nick]] = Time.now 199 | #TODO values are not always deleted; this hash has the possibility to slowly grow and consume more memory 200 | end 201 | stem.message "Your password has been accepted, and you are now authorized.", sender[:nick] 202 | end 203 | end 204 | 205 | # @private 206 | def irc_nick_event(stem, sender, arguments) 207 | @an_lock.synchronize do 208 | revoke stem, sender[:nick] 209 | revoke stem, arguments[:nick] 210 | end 211 | end 212 | 213 | # @private 214 | def irc_kick_event(stem, _, arguments) 215 | @an_lock.synchronize { revoke stem, arguments[:nick] } 216 | end 217 | 218 | # @private 219 | def irc_quit_event(stem, sender, _) 220 | @an_lock.synchronize { revoke stem, sender[:nick] } 221 | end 222 | 223 | # @private 224 | def authenticate(stem, channel, sender, leaf) 225 | @an_lock.synchronize do 226 | if Time.now - @last_protected_action[stem][sender[:nick]] > @expire_time 227 | revoke stem, sender[:nick] 228 | else 229 | @last_protected_action[stem][sender[:nick]] = Time.now 230 | end 231 | @authorized_nicks[stem].include? sender[:nick] 232 | end 233 | end 234 | 235 | def unauthorized 236 | "You must authenticate with an administrator password to do that." 237 | end 238 | 239 | private 240 | 241 | def revoke(stem, nick) 242 | @authorized_nicks[stem].delete nick 243 | @last_protected_action[stem].delete nick 244 | end 245 | end 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /libs/autumn.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'pathname' 3 | 4 | # Container module for all classes of the Autumn IRC bot library. 5 | 6 | module Autumn 7 | # The current Autumn configuration. 8 | Config = OpenStruct.new 9 | 10 | Config.root = Pathname.new(__FILE__).dirname.join('..').realpath 11 | end 12 | -------------------------------------------------------------------------------- /libs/channel_leaf.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # A special kind of {Leaf} that only responds to messages sent to certain 4 | # channels. Leaves that subclass ChannelLeaf can, in their config, specify a 5 | # `channels` option that narrows down which channels the leaf listens to. The 6 | # leaf will not invoke the hook methods nor the `*_command` methods for IRC 7 | # events that are not associated with those channels. It will respond to 8 | # global, non-channel-specific events as well. 9 | # 10 | # You can combine multiple ChannelLeaf subclasses in one {Stem} to allow you 11 | # to run two leaves off of one nick, but have the nick running different 12 | # leaves in different channels. 13 | # 14 | # The `channels` option should be a list of stems, and for each stem, a valid 15 | # channel. For example, if you ran your leaf on two servers, your `stems.yml` 16 | # file might look like: 17 | # 18 | # ```` yaml 19 | # GamingServer: 20 | # channels: fishinggames, games, drivinggames 21 | # # [...] 22 | # FishingServer: 23 | # channels: fishinggames, flyfishing 24 | # # [...] 25 | # ```` 26 | # 27 | # Now let's say you had a trivia leaf that asked questions about fishing 28 | # games. You'd want to run that leaf on the "#fishinggames" channel of each 29 | # server, and the "#games" channel of the GamingServer, but not the other 30 | # channels. (Perhaps your Stem was also running other leaves relevant to those 31 | # channels.) You'd set up your `leaves.yml` file like so: 32 | # 33 | # ```` yaml 34 | # FishingGamesTrivia: 35 | # channels: 36 | # GamingServer: 37 | # - fishinggames 38 | # - games 39 | # FishingServer: fishinggames 40 | # # [...] 41 | # ```` 42 | # 43 | # Now your leaf will only respond to messages relevant to the specified server 44 | # channels (as well as global messages). 45 | # 46 | # Interception and filtering of messages is done at the _leaf_ level, not the 47 | # _stem_ level. Therefore, for instance, if you override 48 | # {Leaf#someone_did_join_channel}, it will only be called for the appropriate 49 | # channels; however, if you implement `irc_join_event`, it will still be 50 | # called for all channels the stem is in. 51 | 52 | class ChannelLeaf < Leaf 53 | # @return [Hash] The IRC channels that this leaf is 54 | # responding to, grouped by Stems handing their servers. 55 | attr :channels 56 | 57 | # @private 58 | def will_start_up 59 | @channels = Hash.new 60 | @options[:channels] ||= Hash.new 61 | @options[:channels].each do |server, chans| 62 | stem = Foliater.instance.stems[server] 63 | raise "Unknown stem #{server}" unless stem 64 | chans = [chans] if chans.kind_of? String 65 | @channels[stem] = chans.map { |chan| stem.normalized_channel_name chan } 66 | end 67 | super 68 | end 69 | 70 | # @private 71 | def irc_privmsg_event(stem, sender, arguments) # :nodoc: 72 | super if arguments[:channel].nil? || listening?(stem, arguments[:channel]) 73 | end 74 | 75 | # @private 76 | def irc_join_event(stem, sender, arguments) # :nodoc: 77 | super if listening?(stem, arguments[:channel]) 78 | end 79 | 80 | # @private 81 | def irc_part_event(stem, sender, arguments) # :nodoc: 82 | super if listening?(stem, arguments[:channel]) 83 | end 84 | 85 | # @private 86 | def irc_mode_event(stem, sender, arguments) # :nodoc: 87 | super if arguments[:channel].nil? || listening?(stem, arguments[:channel]) 88 | end 89 | 90 | # @private 91 | def irc_topic_event(stem, sender, arguments) # :nodoc: 92 | super if listening?(stem, arguments[:channel]) 93 | end 94 | 95 | # @private 96 | def irc_invite_event(stem, sender, arguments) # :nodoc: 97 | super if listening?(stem, arguments[:channel]) || !stem.channels.include?(arguments[:channel]) 98 | end 99 | 100 | # @private 101 | def irc_kick_event(stem, sender, arguments) # :nodoc: 102 | super if listening?(stem, arguments[:channel]) 103 | end 104 | 105 | # @private 106 | def irc_notice_event(stem, sender, arguments) # :nodoc: 107 | super if arguments[:channel].nil? || listening?(stem, arguments[:channel]) 108 | end 109 | 110 | private 111 | 112 | def listening?(stem, channel) 113 | @channels.include? stem && @channels[stem].include?(channel) 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /libs/coder.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # Helper class that generates shell Ruby code. This class knows how to 4 | # generate Ruby code for template classes and methods. 5 | 6 | class Coder # :nodoc: 7 | # @return [String] The generated code string. 8 | attr :output 9 | 10 | # Creates a new instance with an indent level of 0 and an empty output 11 | # string. 12 | 13 | def initialize 14 | @indent = 0 15 | @output = String.new 16 | end 17 | 18 | # Creates a new class empty class. This method yields another Generator, 19 | # which you can populate with the contents of the class, if you wish. 20 | # Example: 21 | # 22 | # ```` ruby 23 | # gen.klass("Foo") { |foo| foo.method("bar") } 24 | # ```` 25 | # 26 | # produces: 27 | # 28 | # ```` 29 | # class Foo 30 | # def bar 31 | # end 32 | # end 33 | # ```` 34 | # 35 | # @param [String] name The class name. 36 | # @param [String] superclass The superclass name, if any. 37 | # @yield [generator] Executes this block in the context of this class's 38 | # contents. 39 | # @yieldparam [Coder] A Coder in the context of this class's contents. 40 | # @return [String] The class code. 41 | 42 | def klass(name, superclass=nil) 43 | if superclass 44 | self << "class #{name} < #{superclass}" 45 | else 46 | self << "class #{name}" 47 | end 48 | 49 | if block_given? 50 | generator = self.class.new 51 | yield generator 52 | indent! 53 | self << generator.output 54 | unindent! 55 | end 56 | 57 | self << 'end' 58 | 59 | return self 60 | end 61 | 62 | # Creates a new empty method. Any additional parameters are considered to be 63 | # the generated method's parameters. They can be symbols/strings (taken to 64 | # be the parameter's name), or hashes associating the parameter's name to 65 | # its default value. 66 | # 67 | # This method yields another Generator, which you can populate with the 68 | # contents of the method, if you wish. Example: 69 | # 70 | # ```` ruby 71 | # gen.method("test", :required, { optional: 'default' }) 72 | # ```` 73 | # 74 | # produces: 75 | # 76 | # ```` 77 | # def test(required, optional="default") 78 | # end 79 | # ```` 80 | # 81 | # @overload method(name, param1, ..., :param2 => default, ...) 82 | # @param [String] name The method name. 83 | # @param [String] param2 A parameter name (with no default). 84 | # @param [String] param2 A parameter name (with default value). 85 | # @param [Object] default A default value for the parameter. 86 | # @yield [coder] Executes this block in the context of this class's 87 | # contents. 88 | # @yieldparam [Coder] coder A Coder in the context of this method's 89 | # contents. 90 | # @return [String] The method code. 91 | 92 | def method(name, *params) 93 | if params.empty? 94 | self << "def #{name}" 95 | else 96 | self << "def #{name}(#{parameterize params})" 97 | end 98 | 99 | if block_given? 100 | generator = self.class.new 101 | yield generator 102 | indent! 103 | self << generator.output 104 | unindent! 105 | end 106 | 107 | self << 'end' 108 | 109 | return self 110 | end 111 | 112 | # Increases the indent level for all future lines of code appended to this 113 | # Coder. 114 | 115 | def indent! 116 | @indent = @indent + 1 117 | end 118 | 119 | # Decreases the indent level for all future lines of code appended to this 120 | # Coder. 121 | 122 | def unindent! 123 | @indent = @indent - 1 unless @indent == 0 124 | end 125 | 126 | # Adds a line of code to this Generator, sequentially. 127 | # 128 | # @param [String] str The line of code. 129 | 130 | def <<(str) 131 | str.split(/\n/).each do |line| 132 | @output << "#{tab}#{line}\n" 133 | end 134 | end 135 | 136 | # Sets this method's documentation and prepends it to the output as a 137 | # comment. 138 | # 139 | # @param [String] str The method documentation. 140 | 141 | def doc=(str) 142 | doc_lines = str.line_wrap(80 - tab.size - 2).split("\n") 143 | doc_lines.map! { |l| "#{tab}# #{l}\n" } 144 | @output = doc_lines.join + "\n" + @output 145 | end 146 | 147 | # Appends a blank line to the output. 148 | 149 | def newline! 150 | @output << "\n" 151 | end 152 | 153 | private 154 | 155 | def parameterize(params) 156 | param_strs = Array.new 157 | params.each do |param| 158 | if param.kind_of? Hash && param.size == 1 159 | name = param.keys.only 160 | default = param.values.only 161 | raise ArgumentError, "Invalid parameter #{name.inspect}" unless name.respond_to?(:to_s) && !name.to_s.empty? 162 | param_strs << "#{name.to_s}=#{default.inspect}" 163 | elsif param.respond_to?(:to_s) && !param.to_s.empty? 164 | param_strs << param.to_s 165 | else 166 | raise ArgumentError, "Invalid parameter #{param.inspect}" 167 | end 168 | end 169 | return param_strs.join(', ') 170 | end 171 | 172 | def tab 173 | ' ' * @indent 174 | end 175 | end 176 | 177 | # Generates Autumn-specific code templates like leaves and filters. 178 | 179 | class TemplateCoder < Coder # :nodoc: 180 | 181 | # Generates a Leaf subclass with the given name. 182 | # 183 | # @param [String] name The Leaf name. 184 | 185 | def leaf(name) 186 | controller = klass('Controller', 'Autumn::Leaf') do |leaf| 187 | leaf.newline! 188 | leaf << '# Typing "!about" displays some basic information about this leaf.' 189 | leaf.newline! 190 | leaf.method('about_command', 'stem', 'sender', 'reply_to', 'msg') do |about| 191 | about << '# This method renders the file "about.txt.erb"' 192 | end 193 | end 194 | controller.doc = "Controller for the #{name.camelcase(:upper)} leaf." 195 | return controller 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /libs/console_boot.rb: -------------------------------------------------------------------------------- 1 | # Used by the script/console script to load the Autumn environment when IRb 2 | # is executed. 3 | 4 | require 'libs/autumn' 5 | require 'libs/genesis' 6 | 7 | @genesis = Autumn::Genesis.new 8 | @genesis.boot! false 9 | -------------------------------------------------------------------------------- /libs/ctcp.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # A listener for a {Stem} that listens for and handles CTCP requests. You can 4 | # add CTCP support for your IRC client by instantiating this object and 5 | # passing it to the {Stem#add_listener} method. 6 | # 7 | # CTCP stands for Client-to-Client Protocol and is a way that IRC clients and 8 | # servers can request and transmit more information about each other. Modern 9 | # IRC clients all have CTCP support, and many servers expect or assume that 10 | # their clients support CTCP. CTCP is also used as a basis for further 11 | # extensions to IRC, such as DCC and XDCC. 12 | # 13 | # This class implements the spec defined at 14 | # http://www.invlogic.com/irc/ctcp.html. 15 | # 16 | # Because some IRC servers will disconnect clients that send a large number of 17 | # messages in a short period of time, this listener will only send one CTCP 18 | # reply per second, with up to a maximum of 10 replies waiting in the queue 19 | # (after which new requests are ignored). These values can be adjusted in the 20 | # initialization options. 21 | # 22 | # This class acts as a listener plugin: Any of the methods specified below can 23 | # be implemented by any other listener, and will be invoked by this listener 24 | # when appropriate. 25 | # 26 | # To respond to incoming CTCP requests, you should implement methods of the 27 | # form `ctcp_*_request`, where "*" is replaced with the lowercase name of the 28 | # CTCP command. (For example, to handle `VERSION` requests, implement 29 | # `ctcp_version_request`). This method will be invoked whenever a request is 30 | # received by the IRC client. It will be given the following parameters: 31 | # 32 | # 1. the CTCP instance that parsed the request, 33 | # 2. the Stem instance that received the request, 34 | # 3. the person who sent the request (a hash in the form of that used by Stem; 35 | # see {Stem#add_listener} for more information), and 36 | # 4. an array of string arguments passed along with the request. 37 | # 38 | # In addition, you can implement `ctcp_request_received`, which will then be 39 | # invoked for any and all incoming CTCP requests. It is passed the following 40 | # arguments: 41 | # 42 | # 1. the name of the request, as a lowercase symbol, 43 | # 2. the CTCP instance that parsed the request, 44 | # 3. the Stem instance that received the request, 45 | # 4. the person who sent the request (a sender hash -- see the {Leaf} docs), 46 | # and 47 | # 5. an array of string arguments passed along with the request. 48 | # 49 | # This class will by default respond to some incoming CTCP requests and 50 | # generate appropriate replies; however, it does not implement any specific 51 | # behavior for parsing incoming replies. If you wish to parse replies, you 52 | # should implement methods in your listener of the form `ctcp_*_response`, 53 | # with the "*" character replaced as above. This method will be invoked 54 | # whenever a reply is received by this listener. You can also implement 55 | # `ctcp_response_received` just as above. The parameters for these methods are 56 | # the same as those listed above. 57 | # 58 | # Responses are assumed to be any CTCP messages that are sent as a `NOTICE` 59 | # (as opposed to a `PRIVMSG`). Because they are `NOTICE`s, your program should 60 | # not send a message in response. 61 | # 62 | # In addition to responding to incoming CTCP requests and replies, your 63 | # listener can use its stem to send CTCP requests and replies. See the 64 | # {#added} method for more detail. 65 | 66 | class CTCP 67 | extend Anise::Annotations 68 | 69 | # Format of an embedded CTCP request. 70 | CTCP_REQUEST = /\x01(.+?)\x01/ 71 | # CTCP commands whose arguments are encoded according to the CTCP spec (as 72 | # opposed to other commands, whose arguments are plaintext). 73 | ENCODED_COMMANDS = %w(VERSION PING) 74 | 75 | # Creates a new CTCP parser. 76 | # 77 | # @param [Hash] options Additional options. 78 | # @option options [Integer] :reply_queue_size (10) The maximum number of 79 | # pending replies to store in the queue, after which new CTCP requests are 80 | # ignored. 81 | # @option options [Float] :reply_rate (0.25) The minimum time, in seconds, 82 | # between consecutive CTCP replies. 83 | 84 | def initialize(options={}) 85 | @options = options 86 | @options[:reply_queue_size] ||= 10 87 | @options[:reply_rate] ||= 0.25 88 | @reply_thread = Hash.new 89 | @reply_queue = Hash.new do |hsh, key| 90 | hsh[key] = ForgetfulQueue.new(@options[:reply_queue_size]) 91 | @reply_thread[key] = Thread.new(key) do |stem| 92 | loop do #TODO wake thread when stem is quitting so this thread can terminate? 93 | reply = @reply_queue[stem].pop 94 | stem.notice reply[:recipient], reply[:message] 95 | sleep @options[:reply_rate] 96 | end 97 | end 98 | hsh[key] 99 | end 100 | end 101 | 102 | # @private Parses CTCP requests in a PRIVMSG event. 103 | 104 | def irc_privmsg_event(stem, sender, arguments) 105 | arguments[:message].scan(CTCP_REQUEST).flatten.each do |ctcp| 106 | ctcp_args = ctcp.split(' ') 107 | request = ctcp_args.shift 108 | ctcp_args = ctcp_args.map { |arg| unquote arg } if ENCODED_COMMANDS.include? request 109 | meth = "ctcp_#{request.downcase}_request".to_sym 110 | stem.broadcast meth, self, stem, sender, ctcp_args 111 | stem.broadcast :ctcp_request_received, request.downcase.to_sym, self, stem, sender, ctcp_args 112 | end 113 | end 114 | 115 | # @private Parses CTCP responses in a NOTICE event. 116 | 117 | def irc_notice_event(stem, sender, arguments) 118 | arguments[:message].scan(CTCP_REQUEST).flatten.each do |ctcp| 119 | ctcp_args = ctcp.split(' ') 120 | request = ctcp_args.shift 121 | ctcp_args = ctcp_args.map { |arg| unquote arg } if ENCODED_COMMANDS.include? request 122 | meth = "ctcp_#{request.downcase}_response".to_sym 123 | stem.broadcast meth, self, stem, sender, ctcp_args 124 | stem.broadcast :ctcp_response_received, request.downcase.to_sym, self, stem, sender, ctcp_args 125 | end 126 | end 127 | 128 | # Replies to a CTCP `CLIENTINFO` request by sending a list of supported CTCP 129 | # commands. This list is generated by introspecting on matching methods. 130 | # 131 | # If a `CLIENTINFO` request is received with a valid command as an argument, 132 | # information on that argument is returned. This is obtained by checking the 133 | # corresponding method's `description` annotation. 134 | 135 | def ctcp_clientinfo_request(handler, stem, sender, arguments) 136 | return unless handler == self 137 | 138 | if arguments.size == 1 139 | command = arguments.only 140 | if (desc = self.class.ann(:"ctcp_#{command.downcase}_request")[:description]) 141 | send_ctcp_reply stem, sender[:nick], 'CLIENTINFO', desc 142 | return 143 | end 144 | end 145 | 146 | commands = self.class.instance_methods.map { |m| m.to_s.match(/^ctcp_(\w+?)_request$/).try(:[], 1) }.compact.map(&:upcase) 147 | send_ctcp_reply stem, sender[:nick], 'CLIENTINFO', "Supported commands: #{commands.join(', ')}" 148 | end 149 | ann :ctcp_clientinfo_request, description: "Returns a list of valid CTCP commands, or information on a specific CTCP command." 150 | 151 | # Replies to a CTCP `VERSION` request by sending: 152 | # 153 | # * the name of the IRC client ("Autumn, a Ruby IRC framework"), 154 | # * the operating system name and version, and 155 | # * the home page URL for Autumn. 156 | # 157 | # Although the CTCP spec states that the `VERSION` response should be three 158 | # encoded strings (as shown above), many modern clients expect one plaintext 159 | # string. If you'd prefer compatibility with those clients, you should 160 | # override this method to return a single plaintext string and remove the 161 | # `VERSION` command from {ENCODED_COMMANDS}. 162 | 163 | def ctcp_version_request(handler, stem, sender, _) 164 | return unless handler == self 165 | send_ctcp_reply stem, sender[:nick], 'VERSION', "Autumn #{Autumn::Config.version}, a Ruby IRC framework", RUBY_PLATFORM, 'http://github.com/RISCfuture/autumn' 166 | end 167 | ann :ctcp_version_request, description: "Returns information on this IRC client." 168 | 169 | # Replies to a CTCP `PING` request by sending back the same arguments as a 170 | # `PONG` reply. 171 | 172 | def ctcp_ping_request(handler, stem, sender, arguments) 173 | return unless handler == self 174 | send_ctcp_reply stem, sender[:nick], 'PING', *arguments 175 | end 176 | ann :ctcp_ping_request, description: "Returns a PING response." 177 | 178 | # Replies to a CTCP `TIME` request by sending back the local time in RFC 822 179 | # format. 180 | 181 | def ctcp_time_request(handler, stem, sender, _) 182 | return unless handler == self 183 | send_ctcp_reply stem, sender[:nick], 'TIME', Time.now.rfc822 184 | end 185 | ann :ctcp_time_request, description: "Returns the current client time, in RFC 822 (XML) format." 186 | 187 | # Replies to a CTCP `SOURCE` request by sending the Autumn GitHub repository 188 | # home page. 189 | 190 | def ctcp_source_request(handler, stem, sender, _) 191 | return unless handler == self 192 | send_ctcp_reply stem, sender[:nick], 'SOURCE', 'http://github.com/RISCfuture/autumn' 193 | end 194 | ann :ctcp_source_request, description: "Returns the URL where Autumn can be downloaded." 195 | 196 | # Adds a CTCP reply to the queue. You must pass the Stem instance that will 197 | # be sending this reply, the recipient (channel or nick), and the name of 198 | # the CTCP command (as an uppercase string). Any additional arguments are 199 | # taken to be arguments of the CTCP reply, and are thus encoded and joined 200 | # by space characters, as specified in the CTCP white paper. The arguments 201 | # should all be strings. 202 | # 203 | # @param [Stem] stem The Stem that will be sending the reply. 204 | # @param [String, Hash] recipient The channel name, nickname, or sender hash 205 | # to send the reply to. 206 | # @param [String] command The CTCP command being replied to. 207 | # @param [Array] arguments Additional arguments to encode as part of the 208 | # reply. Encoding of arguments is only done for commands in 209 | # {ENCODED_COMMANDS}. 210 | 211 | def send_ctcp_reply(stem, recipient, command, *arguments) 212 | recipient = recipient[:nick] if recipient.kind_of? Hash 213 | @reply_queue[stem] << { recipient: recipient, message: make_ctcp_message(command, *arguments) } 214 | end 215 | 216 | # When this listener is added to a stem, the stem gains the ability to send 217 | # CTCP messages directly. Methods of the form `ctcp_*`, where "*" is the 218 | # lowercase name of a CTCP action, will be forwarded to this listener, which 219 | # will send the CTCP message. The first parameter of the method is the nick 220 | # of one or more recipients; all other parameters are parameters for the 221 | # CTCP command. See the CTCP spec for more information on the different 222 | # commands and parameters available. 223 | # 224 | # For example, to send an action (such as "/me is cold") to a channel: 225 | # 226 | # ```` ruby 227 | # stem.ctcp_action "#channel", "is cold" 228 | # ```` 229 | # 230 | # In addition, the stem gains the ability to send CTCP replies. Replies are 231 | # messages that are added to this class's reply queue, adding flood abuse 232 | # prevention. To send a reply, call a Stem method of the form 233 | # `ctcp_reply_*`, where "*" is the command name you are replying to, in 234 | # lowercase. Pass first the nick or sender hash of the recipient, then any 235 | # any parameters as specified by the CTCP spec. For example, to respond to a 236 | # CTCP `VERSION` request: 237 | # 238 | # ```` ruby 239 | # stem.ctcp_reply_version sender, 'Bot Name', 'Computer Name', 'Other Info' 240 | # ```` 241 | # 242 | # (Note that responding to VERSION requests is already handled by this class 243 | # so you'll need to either override or delete the {#ctcp_version_request} 244 | # method to do this.) 245 | # 246 | # @param [Stem] stem The stem this listener was added to. 247 | 248 | def added(stem) 249 | stem.instance_variable_set :@ctcp, self 250 | class << stem 251 | def method_missing(meth, *args) 252 | if meth.to_s =~ /^ctcp_reply_([a-z]+)$/ 253 | @ctcp.send_ctcp_reply self, args.shift, $1.to_s.upcase, *args 254 | elsif meth.to_s =~ /^ctcp_([a-z]+)$/ 255 | privmsg args.shift, @ctcp.make_ctcp_message($1.to_s.upcase, *args) 256 | else 257 | super 258 | end 259 | end 260 | end 261 | end 262 | 263 | # @private 264 | # 265 | # Creates a CTCP-formatted message with the given command (uppercase string) 266 | # and arguments (strings). The string returned is suitable for transmission 267 | # over IRC using the `PRIVMSG` command. 268 | 269 | def make_ctcp_message(command, *arguments) 270 | arguments = arguments.map { |arg| quote arg } if ENCODED_COMMANDS.include? command 271 | "\01#{arguments.unshift(command).join(' ')}\01" 272 | end 273 | 274 | private 275 | 276 | def quote(str) 277 | chars = str.split('') 278 | chars.map! do |char| 279 | case char 280 | when "\0" then '\0' 281 | when "\1" then '\1' 282 | when "\n" then '\n' 283 | when "\r" then '\r' 284 | when ' ' then '\@' 285 | when "\\" then '\\\\' 286 | else char 287 | end 288 | end 289 | return chars.join 290 | end 291 | 292 | def unquote(str) 293 | str.gsub('\\\\', '\\').gsub('\@', ' ').gsub('\r', "\r").gsub('\n', "\n").gsub('\1', "\1").gsub('\0', "\0") 294 | end 295 | end 296 | end 297 | -------------------------------------------------------------------------------- /libs/daemon.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # Describes an IRC server daemon program. Different IRC servers run off of 4 | # different IRC daemons, each of which has a slightly different implementation 5 | # of the IRC protocol. To encapsulate this, the Daemon class stores the names 6 | # of some of the more common IRC server types, as well as the unique 7 | # information about those daemons, such as supported usermodes, response 8 | # codes, and supported channel types. 9 | # 10 | # An instance of Daemon is an IRC server type. The Daemon class keeps a 11 | # catalog of all instances, assigning each a descriptive name (for example, 12 | # "Unreal" for the UnrealIRCd program, a popular IRC server daemon). 13 | # 14 | # A Daemon instance can be queried for information about the IRC server type, 15 | # as necessary to parse messages from that IRC server. 16 | # 17 | # A "default" daemon will also be created representing a common denominator of 18 | # IRC features, which is used in situations where a server's exact type is 19 | # unknown. This daemon will consist of all non-conflicting entries among the 20 | # defined daemons. 21 | # 22 | # In addition to the methods and attributes below, you can also call predicate 23 | # methods such as `usermode?` and `channel_prefix?` to test if a character is 24 | # in a set of known modes/privileges/prefixes, or if a number is in the set of 25 | # known events. 26 | 27 | class Daemon 28 | 29 | # Creates a new Daemon instance associated with a given name. You must also 30 | # pass in the hashes for it to store. 31 | # 32 | # @param [String, nil] name The name of the IRC server daemon, or `nil` if 33 | # we're creating the default Daemon. 34 | # @param [Hash] info The IRC server daemon info, loaded from the YAML file, 35 | # or `nil` if we're creating the default daemon. 36 | 37 | def initialize(name, info) 38 | if name.nil? && info.nil? # it's the default hash 39 | raise "Already created a default Daemon" if self.class.class_variable_defined? :@@default 40 | @usermode = Hash.parroting 41 | @privilege = Hash.parroting 42 | @user_prefix = Hash.parroting 43 | @channel_prefix = Hash.parroting 44 | @channel_mode = Hash.parroting 45 | @server_mode = Hash.parroting 46 | @event = Hash.parroting 47 | @default = true 48 | else 49 | @usermode = Hash.parroting(info['usermode']) 50 | @privilege = Hash.parroting(info['privilege']) 51 | @user_prefix = Hash.parroting(info['user_prefix']) 52 | @channel_prefix = Hash.parroting(info['channel_prefix']) 53 | @channel_mode = Hash.parroting(info['channel_mode']) 54 | @server_mode = Hash.parroting(info['server_mode']) 55 | @event = Hash.parroting(info['event']) 56 | @@instances[name] = self 57 | 58 | # Build up our default so it contains all keys with no conflicting 59 | # values across different server specs. Delete keys from the default 60 | # hash for which we found duplicates. 61 | info.each do |hname, hsh| 62 | next unless @@default.respond_to? hname.to_sym 63 | default_hash = @@default.send(hname.to_sym) 64 | 65 | uniques = hsh.reject { |k, _| default_hash.include? k } 66 | default_hash.update uniques 67 | default_hash.reject! { |k, v| hsh.include?(k) && hsh[k] != v } 68 | end 69 | end 70 | end 71 | 72 | # Finds a Daemon instance by associated name. 73 | # 74 | # @param [String] name A Daemon name. 75 | # @return [Autumn::Daemon] A Daemon instance. 76 | 77 | def self.[](name) 78 | @@instances[name] 79 | end 80 | 81 | # @return [Autumn::Daemon] The fallback server type. 82 | 83 | def self.default 84 | @@default 85 | end 86 | 87 | # Yields the name of each Daemon registered with the class. 88 | # 89 | # @yield [daemon] A block to pass to each Daemon. 90 | # @yieldparam [Autumn::Daemon] daemon A Daemon instance. 91 | 92 | def self.each_name 93 | @@instances.keys.sort.each { |name| yield name } 94 | end 95 | 96 | # @return [Hash] A hash of usermode characters (e.g., `i`) 97 | # to their symbol representations (e.g., `:invisible`). 98 | 99 | def usermode 100 | @default ? @usermode : @@default.usermode.merge(@usermode) 101 | end 102 | 103 | # @return [Hash] A hash of user privilege characters (e.g., 104 | # `v`) to their symbol representations (e.g., `:voiced`). 105 | 106 | def privilege 107 | @default ? @privilege : @@default.privilege.merge(@privilege) 108 | end 109 | 110 | # @return [Hash] A hash of user privilege prefixes (e.g., 111 | # `@`) to their symbol representations (e.g., `:operator`). 112 | 113 | def user_prefix 114 | @default ? @user_prefix : @@default.user_prefix.merge(@user_prefix) 115 | end 116 | 117 | # @return [Hash] A hash of channel prefixes (e.g., `&`) to 118 | # their symbol representations (e.g., `:local`). 119 | 120 | def channel_prefix 121 | @default ? @channel_prefix : @@default.channel_prefix.merge(@channel_prefix) 122 | end 123 | 124 | # @return [Hash] A hash of channel mode characters (e.g., 125 | # `m`) to their symbol representations (e.g., `:moderated`). 126 | 127 | def channel_mode 128 | @default ? @channel_mode : @@default.channel_mode.merge(@channel_mode) 129 | end 130 | 131 | # @return [Hash] A hash of server mode characters (e.g., 132 | # `H`) to their symbol representations (e.g., `:hidden`). 133 | 134 | def server_mode 135 | @default ? @server_mode : @@default.server_mode.merge(@server_mode) 136 | end 137 | 138 | # @return [Hash] A hash of numerical event codes (e.g., 139 | # 372) to their symbol representations (e.g., `:motd`). 140 | 141 | def event 142 | @default ? @event : @@default.event.merge(@event) 143 | end 144 | 145 | # Returns true if the mode string (e.g., "+v") appears to be changing a user 146 | # privilege as opposed to a channel mode. 147 | # 148 | # @param [String] mode A mode string. 149 | # @return [true, false] Whether this appears to be a user mode. 150 | 151 | def privilege_mode?(mode) 152 | raise ArgumentError, "Invalid mode string '#{mode}'" unless mode =~ /^[\+\-]\S+$/ 153 | mode.except_first.chars.all? { |c| privilege? c } 154 | end 155 | 156 | # Returns true if the first character(s) of a nick are valid privilege 157 | # prefixes. 158 | # 159 | # @param [String] nick A nickname. 160 | # @return [true, false] Whether the nick is prefixed with a privilege 161 | # indicator. 162 | 163 | def nick_prefixed?(nick) 164 | user_prefix? nick[0, 1] 165 | end 166 | 167 | # Given a nick, returns that nick stripped of any privilege characters on 168 | # the left side. 169 | # 170 | # @param [String] name A nickname. 171 | # @return [String] The nickname without a privilege prefix. 172 | 173 | def just_nick(name) 174 | nick = name.dup 175 | while nick_prefixed?(nick) 176 | nick.slice! 0, 1 177 | end 178 | return nick 179 | end 180 | 181 | # Given a nick, returns the privileges granted to this nick, as indicated by 182 | # the prefix characters. Returns `:unvoiced` if no prefix characters are 183 | # present. Returns an array of privileges if multiple prefix characters are 184 | # present. 185 | # 186 | # @param [String] name A nickname (including privilege prefix). 187 | # @return [Symbol, Array] One or more privileges indicated by this 188 | # nick's privilege prefix. 189 | 190 | def nick_privilege(name) 191 | privs = Set.new 192 | nick = name.dup 193 | while user_prefix? nick[0, 1] 194 | privs << user_prefix[nick[0, 1]] 195 | nick.slice! 0, 1 196 | end 197 | case privs.size 198 | when 0 then :unvoiced 199 | when 1 then privs.only 200 | else privs 201 | end 202 | end 203 | 204 | # @private 205 | def method_missing(meth, *args) 206 | if meth.to_s =~ /^([a-z_]+)\?$/ 207 | base = $1 208 | if (instance_variables.include?("@#{base}") || instance_variables.include?("@#{base}".to_sym)) && args.size == 1 209 | if base.end_with?('_prefix') && args.only.kind_of?(Numeric) then 210 | arg = args.only.chr 211 | else 212 | arg = args.only 213 | end 214 | eval "#{base}.include? #{arg.inspect}" 215 | end 216 | else 217 | super 218 | end 219 | end 220 | 221 | # @private 222 | def inspect 223 | "#<#{self.class.to_s} #{@@instances.key self}>" 224 | end 225 | 226 | private 227 | 228 | @@instances = Hash.new 229 | @@default = self.new(nil, nil) 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /libs/datamapper_hacks.rb: -------------------------------------------------------------------------------- 1 | # A set of hacks to make DataMapper play more nicely with classes within 2 | # modules. 3 | 4 | DataMapper::Repository.class_eval do # :nodoc: 5 | 6 | # @private 7 | # 8 | # Add a method to return all models defined for a repository. 9 | 10 | def models 11 | DataMapper::Model.descendants.select { |cl| !cl.properties(name).empty? || !cl.relationships(name).empty? } 12 | #HACK we are assuming that if a model has properties or relationships 13 | # defined for a repository, then it must be contextual to that repo 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /libs/foliater.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # Loads Stems and Leaves and executes them in their own threads. Manages the 4 | # threads and oversees all leaves. This is a singleton class. 5 | 6 | class Foliater 7 | include Singleton 8 | 9 | # @return [Speciator] The Speciator singleton. 10 | attr_reader :config 11 | # @return [Hash] A hash of all Stem instances by their config 12 | # names. 13 | attr_reader :stems 14 | # @return [Hash] A hash of all Leaf instances by their config 15 | # names. 16 | attr_reader :leaves 17 | 18 | # @private 19 | def initialize 20 | @config = Speciator.instance 21 | @stems = Hash.new 22 | @leaves = Hash.new 23 | @ctcp = Autumn::CTCP.new 24 | end 25 | 26 | # Loads the config files and their classes, initializes all stems and leaves 27 | # and begins the stems' execution processes in their own threads. You must 28 | # pass the stem and leaf config hashes (from the `stems.yml` and 29 | # `leaves.yml` files). 30 | # 31 | # @param [Hash] stem_config The stem configuration hash loaded from 32 | # `stems.yml`. 33 | # @param [Hash] leaf_config The stem configuration hash loaded from 34 | # `leaves.yml`. 35 | # @param [true, false] invoke If set to `false`, {#start_stems} will not be 36 | # called (used for loading an interactive console). 37 | 38 | def load(stem_config, leaf_config, invoke=true) 39 | load_configs stem_config, leaf_config 40 | load_leaf_classes 41 | load_leaves 42 | load_all_leaf_models 43 | load_stems 44 | start_stems if invoke 45 | end 46 | 47 | # Reloads a Leaf while it is running. Re-opens class definition files and 48 | # runs them to redefine the classes. Does not work exactly as it should, 49 | # but well enough for a rough hot-reload capability. 50 | # 51 | # @param [Leaf] leaf A Leaf to hot-reload. 52 | 53 | def hot_reload(leaf) 54 | type = leaf.class.to_s.split('::').first 55 | load_leaf_libs type 56 | load_leaf_controller type 57 | load_leaf_helpers type 58 | load_leaf_models leaf 59 | load_leaf_views type 60 | end 61 | 62 | # @return [true, false] `true` if there is at least one Stem still running. 63 | 64 | def alive? 65 | @stem_threads && @stem_threads.any? { |_, thread| thread.alive? } 66 | end 67 | 68 | # This method yields each Stem that was loaded, allowing you to iterate over 69 | # each stem. 70 | # 71 | # @yield [stem] A block to execute for each Stem. 72 | # @yieldparam [Stem] stem Each Stem, in turn. 73 | # 74 | # @example Take attendance: 75 | # Foliater.instance.each_stem { |stem| stem.message "Here!" } 76 | 77 | def each_stem 78 | @stems.each { |stem| yield stem } 79 | end 80 | 81 | # This method yields each Leaf that was loaded, allowing you to iterate over 82 | # each stem. 83 | # 84 | # @yield [leaf] A block to execute for each Leaf. 85 | # @yieldparam [Leaf] leaf Each Leaf, in turn. 86 | # 87 | # @example Take attendance: 88 | # Foliater.instance.each_leaf { |leaf| leaf.stems.message "Here!" } 89 | 90 | def each_leaf 91 | @leaves.each { |leaf| yield leaf } 92 | end 93 | 94 | private 95 | 96 | def load_configs(stem_config, leaf_config) 97 | leaf_config.each do |name, options| 98 | global_config_file = Autumn::Config.root.join('leaves', options['class'].snakecase, 'config.yml') 99 | if global_config_file.exist? 100 | config.leaf name, YAML.load_file(global_config_file) 101 | end 102 | config.leaf name, options 103 | config.leaf name, logger: LogFacade.new(config.global(:logfile), 'Leaf', name) 104 | end 105 | stem_config.each do |name, options| 106 | config.stem name, options 107 | config.stem name, logger: LogFacade.new(config.global(:logfile), 'Stem', name) 108 | end 109 | end 110 | 111 | def load_leaf_classes 112 | config.all_leaf_classes.each do |type| 113 | Object.class_eval "module #{type}; end" 114 | 115 | config.leaf type, module: Object.const_get(type) 116 | 117 | load_leaf_libs type 118 | load_leaf_controller(type) 119 | load_leaf_helpers(type) 120 | load_leaf_views(type) 121 | end 122 | end 123 | 124 | def load_leaf_controller(type) 125 | controller_file = Autumn::Config.root.join('leaves', type.snakecase, 'controller.rb') 126 | raise "controller.rb file for leaf #{type} not found" unless controller_file.exist? 127 | controller_code = nil 128 | begin 129 | File.open(controller_file, 'r') { |f| controller_code = f.read } 130 | rescue Errno::ENOENT 131 | raise "controller.rb file for leaf #{type} not found" 132 | end 133 | config.leaf(type, :module).module_eval controller_code, controller_file.realpath.to_s 134 | end 135 | 136 | def load_leaf_helpers(type) 137 | mod = config.leaf(type, :module) 138 | helper_code = nil 139 | helper_modules = Array.new 140 | Autumn::Config.root.join('leaves', type.snakecase, 'helpers').glob('*.rb').each do |helper_file| 141 | File.open(helper_file, 'r') { |f| helper_code = f.read } 142 | mod.module_eval helper_code 143 | helper_modules << (helper_file.basename('.rb').to_s.camelcase(:upper) + 'Helper').to_sym 144 | end 145 | 146 | begin 147 | mod.const_get('Controller') 148 | rescue NameError 149 | raise NameError, "Couldn't find Controller class for leaf #{type}" 150 | end 151 | 152 | config.leaf type, helpers: Set.new 153 | helper_modules.each do |mod_name| 154 | helper_module = mod.const_get(mod_name) rescue next 155 | config.leaf(type, :helpers) << helper_module 156 | end 157 | end 158 | 159 | def load_leaf_libs(type) 160 | Bundler.require type.snakecase.to_sym 161 | Autumn::Config.root.join('leaves', type.snakecase, 'lib').glob('*.rb').each { |lib_file| require lib_file } 162 | end 163 | 164 | def load_leaf_views(type) 165 | views = Hash.new 166 | view_text = nil 167 | Autumn::Config.root.join('leaves', type.snakecase, 'views').glob('*.txt.erb').each do |view_file| 168 | view_name = view_file.basename.to_s.match(/^(.+)\.txt\.erb$/)[1] 169 | File.open(view_file, 'r') { |f| view_text = f.read } 170 | views[view_name] = view_text 171 | end 172 | config.leaf type, views: views 173 | end 174 | 175 | def load_leaves 176 | config.each_leaf do |name, _| 177 | options = config.options_for_leaf(name) 178 | options[:root] = config.global(:root).join('leaves', options[:class].snakecase) 179 | begin 180 | leaf_class = options[:module].const_get('Controller') 181 | rescue NameError 182 | raise NameError, "Couldn't find Controller class for leaf #{name}" 183 | end 184 | @leaves[name] = leaf_class.new(options) 185 | formatter = Autumn::Formatting.const_get options[:formatter].to_sym if options[:formatter] && (Autumn::Formatting.constants.include? options[:formatter] || Autumn::Formatting.constants.include?(options[:formatter].to_sym)) 186 | formatter ||= Autumn::Formatting::DEFAULT 187 | @leaves[name].extend formatter 188 | options[:helpers].each { |helper| @leaves[name].extend helper } 189 | # extend the formatter first so helper methods override its methods if necessary 190 | end 191 | end 192 | 193 | def load_all_leaf_models 194 | @leaves.each { |_, instance| load_leaf_models instance } 195 | end 196 | 197 | def load_leaf_models(leaf) 198 | model_code = nil 199 | mod = config.leaf(leaf.options[:class], :module) 200 | leaf.database do 201 | Autumn::Config.root.join('leaves', leaf.options[:class].snakecase, 'models').glob('*.rb').each do |model_file| 202 | File.open(model_file, 'r') { |f| model_code = f.read } 203 | mod.module_eval model_code, model_file.realpath 204 | end 205 | # Need to manually set the table names of the models because we loaded 206 | # them inside a module 207 | unless Autumn::Config.no_database 208 | mod.constants.map { |const_name| mod.const_get(const_name) }.select { |const| const.ancestors.include? DataMapper::Resource }.each do |model| 209 | model.storage_names[leaf.database_name] = model.to_s.demodulize.snakecase.pluralize 210 | end 211 | end 212 | end 213 | end 214 | 215 | def load_stems 216 | config.each_stem do |name, _| 217 | options = config.options_for_stem(name) 218 | server = options[:server] 219 | nick = options[:nick] 220 | 221 | @stems[name] = Stem.new(server, nick, options) 222 | leaves = options[:leaves] 223 | leaves ||= [options[:leaf]] 224 | leaves.each do |leaf| 225 | raise "Unknown leaf #{leaf} in configuration for stem #{name}" unless @leaves[leaf] 226 | @stems[name].add_listener @leaves[leaf] 227 | @stems[name].add_listener @ctcp 228 | #TODO a configurable way of specifying listeners to add by default 229 | @leaves[leaf].stems << @stems[name] 230 | end 231 | end 232 | end 233 | 234 | def start_stems 235 | @leaves.each { |_, leaf| leaf.preconfigure } 236 | @leaves.each { |_, leaf| leaf.will_start_up } 237 | @stem_threads = Hash.new 238 | config.each_stem do |name, options| 239 | @stem_threads[name] = Thread.new(@stems[name], Thread.current) do |stem, parent_thread| 240 | # The thread will run the stem until it exits, then inform the main 241 | # thread that it has exited. When the main thread wakes, it checks if 242 | # all stems have terminated; if so, it terminates itself. 243 | begin 244 | stem.start 245 | rescue 246 | options[:logger].fatal $! 247 | ensure 248 | parent_thread.wakeup # Schedule the parent thread to wake up after this thread finishes 249 | end 250 | end 251 | end 252 | end 253 | 254 | end 255 | end 256 | -------------------------------------------------------------------------------- /libs/formatting.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # Adds text formatting to Autumn objects. Text formatting (color and styles) 4 | # is not a part of the original IRC spec, so many clients have come up with 5 | # many different ways of sending formatted text. The classes in this module 6 | # encapsulate those various methods. 7 | # 8 | # To add formatting to a stem or leaf, simply include the appropriate module 9 | # in your Leaf subclass, Stem instance, or other Autumn object. You can also 10 | # use these constants directly from the module, without adding them into your 11 | # class. 12 | # 13 | # Where possible, all modules in the Formatting module follow an implicit 14 | # protocol, which includes methods like `color`, `bold`, `plain`, and 15 | # `underline`. 16 | 17 | module Formatting 18 | 19 | # The mIRC format is the oldest IRC text formatting protocol, written for 20 | # use with the mIRC client. Although mIRC formatting is by far the most 21 | # common and most widely supported, it is also has the fewest features. mIRC 22 | # also has some limitations that can occur when coloring text; please see 23 | # the color method for more information. 24 | # 25 | # To stylize your text, insert the appropriate style code in your text where 26 | # desired. For example (assuming you have `include`d the Mirc module): 27 | # 28 | # ```` ruby 29 | # "I'm feeling #{BOLD}bold today, and #{ITALIC}how#{PLAIN}!" 30 | # ```` 31 | # 32 | # yields: 33 | # 34 | # I'm feeling **bold today, and _how_**! 35 | # 36 | # To colorize text, you must call the color method, and insert an {UNCOLOR} 37 | # token at the end of the colorized text: 38 | # 39 | # ```` ruby 40 | # "The system is: #{color(:red)}down#{UNCOLOR}!" 41 | # ```` 42 | 43 | module Mirc 44 | # Insert this character to set all following text unformatted. 45 | PLAIN = 15.chr 46 | # Insert this character to set all following text bolded. 47 | BOLD = 2.chr 48 | # Insert this character to set all following text italicized. 49 | ITALIC = 22.chr 50 | # Insert this character to set all following text underlined. 51 | UNDERLINE = 31.chr 52 | 53 | # The mIRC color code sentinel. 54 | COLOR_CODE = 3.chr 55 | # Insert this character to stop colorizing text. 56 | UNCOLOR = COLOR_CODE + ' ' 57 | # Same as `UNCOLOR`, but suppresses the trailing space for situations 58 | # where no conflict is assured. 59 | UNCOLOR_NO_SPACE = COLOR_CODE 60 | # Valid IRC colors, in the mIRC style, to be used with the color method. 61 | COLORS = { 62 | white: '00', 63 | black: '01', 64 | dark_blue: '02', 65 | navy_blue: '02', 66 | dark_green: '03', 67 | red: '04', 68 | brown: '05', 69 | dark_red: '05', 70 | purple: '06', 71 | dark_yellow: '07', 72 | olive: '07', 73 | orange: '07', 74 | yellow: '08', 75 | green: '09', 76 | lime: '09', 77 | dark_cyan: '10', 78 | teal: '10', 79 | cyan: '11', 80 | blue: '12', 81 | royal_blue: '12', 82 | magenta: '13', 83 | pink: '13', 84 | fuchsia: '13', 85 | gray: '14', 86 | light_gray: '15', 87 | silver: '15' 88 | } 89 | 90 | # Colors the following text with a foreground and background color. Colors 91 | # are a symbol in the {COLORS} hash. By default the background is left 92 | # uncolored. This method returns a string that should be prepended to the 93 | # text you want to colorize. Append an {UNCOLOR} token when you wish to 94 | # end colorization. 95 | # 96 | # Because of limitations in the mIRC color-coding system, a space will be 97 | # added after the color code (and before any colorized text). Without this 98 | # space character, it is possible that your text will appear in the wrong 99 | # color. (This is most likely to happen when colorizing numbers with 100 | # commas in them, such as "1,160".) If you would like to suppress this 101 | # space, because you either are sure that your text will be formatted 102 | # correctly anyway, or you simply don't care, you can pass 103 | # `suppress_space: true` to this method. 104 | # 105 | # @param [Symbol] fgcolor The foreground color. 106 | # @param [Symbol] bgcolor The background color. 107 | # @param [Hash] options Additional options. 108 | # @option options [true, false] :suppress_space (false) If `false`, a 109 | # space is added after the colorization text to ensure that no 110 | # formatting errors occur. 111 | 112 | def color(fgcolor, bgcolor=nil, options={}) 113 | fgcolor = :black unless COLORS.include? fgcolor 114 | bgcolor = :white unless (bgcolor.nil? || COLORS.include?(bgcolor)) 115 | "#{COLOR_CODE}#{COLORS[fgcolor]}#{bgcolor ? (',' + COLORS[bgcolor]) : ''}#{options[:suppress_space] ? '' : ' '}" 116 | end 117 | 118 | # Sets all following text unformatted. 119 | def plain() PLAIN end 120 | 121 | # Sets all following text bold. 122 | def bold() BOLD end 123 | 124 | # Sets all following text italic. 125 | def italic() ITALIC end 126 | 127 | # Sets all following text underline. 128 | def underline() UNDERLINE end 129 | 130 | # Removes coloring from all following text. 131 | # 132 | # @param [Hash] options Additional options. 133 | # @option options [true, false] :suppress_space (false) If `false`, a 134 | # space is added after the colorization text to ensure that no 135 | # formatting errors occur. 136 | 137 | def uncolor(options={}) 138 | options[:suppress_space] ? UNCOLOR_NO_SPACE : UNCOLOR 139 | end 140 | 141 | # Removes formatting from a string. Due to mIRC's formatting quirkiness, 142 | # this may leave the original string with some extra space characters here 143 | # and there. 144 | # 145 | # @param [String] str A color-formatted string. 146 | # @return [String] The unformatted, plain string. 147 | 148 | def deformat(str) 149 | str.gsub(/#{Regexp.escape COLOR_CODE}(\d{2}(,\d{2})?)?/, '').gsub(PLAIN, '').gsub(BOLD, '').gsub(ITALIC, '').gsub(UNDERLINE, '') 150 | end 151 | end 152 | 153 | # The default formatter for leaves that do not specify otherwise. 154 | DEFAULT = Mirc 155 | 156 | # The ircle formatting system is an adaptation of the mIRC system, written 157 | # for use by the ircle Macintosh client. Its primary purpose is to improve 158 | # upon mIRC's lackluster color support. The ircle protocol is identical to 159 | # the mIRC protocol for purposes of text styling (bold, italic, underline), 160 | # so stylized text will appear the same on both clients. 161 | # 162 | # The only difference is in text colorization, for which ircle has a 163 | # slightly better system, but one that is incompatible with mIRC-type 164 | # clients. 165 | # 166 | # Styling text is done exactly as it is in the Mirc module; coloring text is 167 | # done with the COLORS hash, as so: 168 | # 169 | # ```` ruby 170 | # "The system is: #{COLORS[:red]}down#{PLAIN}!" 171 | # ```` 172 | # 173 | # Note that there is no support for background coloring. 174 | 175 | module Ircle 176 | # Insert this character to set all following text unformatted and 177 | # uncolored. 178 | PLAIN = 15.chr 179 | # Insert this character to set all following text bolded. 180 | BOLD = 2.chr 181 | # Insert this character to set all following text italicized. 182 | ITALIC = 22.chr 183 | # Insert this character to set all following text underlined. 184 | UNDERLINE = 31.chr 185 | # The ircle color code sentinel. 186 | COLOR_CODE = 3.chr 187 | # Insert a character from this hash to set the color of all following 188 | # text. 189 | COLORS = { 190 | white: COLOR_CODE + '0', 191 | black: COLOR_CODE + '1', 192 | red: COLOR_CODE + '2', 193 | orange: COLOR_CODE + '3', 194 | yellow: COLOR_CODE + '4', 195 | light_green: COLOR_CODE + '5', 196 | green: COLOR_CODE + '6', 197 | blue_green: COLOR_CODE + '7', 198 | cyan: COLOR_CODE + '8', 199 | light_blue: COLOR_CODE + '9', 200 | blue: COLOR_CODE + ':', 201 | purple: COLOR_CODE + ';', 202 | magenta: COLOR_CODE + '<', 203 | purple_red: COLOR_CODE + '=', 204 | light_gray: COLOR_CODE + '>', 205 | dark_gray: COLOR_CODE + '?', 206 | dark_red: COLOR_CODE + '@', 207 | dark_orange: COLOR_CODE + 'A', 208 | dark_yellow: COLOR_CODE + 'B', 209 | dark_light_green: COLOR_CODE + 'C', 210 | dark_green: COLOR_CODE + 'D', 211 | dark_blue_green: COLOR_CODE + 'E', 212 | dark_cyan: COLOR_CODE + 'F', 213 | dark_light_blue: COLOR_CODE + 'G', 214 | dark_blue: COLOR_CODE + 'H', 215 | dark_purple: COLOR_CODE + 'I', 216 | dark_magenta: COLOR_CODE + 'J', 217 | dark_purple_red: COLOR_CODE + 'K', 218 | # User-defined colors: 219 | server_message: COLOR_CODE + 'a', 220 | standard_message: COLOR_CODE + 'b', 221 | private_message: COLOR_CODE + 'c', 222 | notify: COLOR_CODE + 'd', 223 | dcc_ctcp: COLOR_CODE + 'e', 224 | window_bg: COLOR_CODE + 'f', 225 | own_message: COLOR_CODE + 'g', 226 | notice: COLOR_CODE + 'h', 227 | user_highlight: COLOR_CODE + 'i', 228 | userlist_chanop: COLOR_CODE + 'l', 229 | userlist_ircop: COLOR_CODE + 'm', 230 | userlist_voice: COLOR_CODE + 'n' 231 | } 232 | # For purposes of cross-compatibility, this constant has been added to 233 | # match the Mirc module. Removes all formatting and coloring on all 234 | # following text. 235 | UNCOLOR = PLAIN 236 | 237 | # For purposes of cross-compatibility, this method has been added to match 238 | # the Mirc method with the same name. All inapplicable parameters and 239 | # color names are ignored. 240 | # 241 | # @param [Symbol] fgcolor The foreground color. 242 | # @param [Object] bgcolor Ignored. 243 | # @param [Object] options Ignored. 244 | 245 | def color(fgcolor, bgcolor=nil, options={}) 246 | COLORS[fgcolor] 247 | end 248 | 249 | # Sets all following text unformatted. 250 | def plain() PLAIN end 251 | 252 | # Sets all following text bold. 253 | def bold() BOLD end 254 | 255 | # Sets all following text italic. 256 | def italic() ITALIC end 257 | 258 | # Sets all following text underline. 259 | def underline() UNDERLINE end 260 | 261 | # Removes formatting from a string. 262 | # 263 | # @param [String] str The colored string. 264 | # @return [String] The unformatted, plain string. 265 | 266 | def deformat(str) 267 | str.gsub(/#{Regexp.escape COLOR_CODE}(\d{2}(,\d{2})?)?/, '').gsub(PLAIN, '').gsub(BOLD, '').gsub(ITALIC, '').gsub(UNDERLINE, '') 268 | end 269 | end 270 | end 271 | end 272 | -------------------------------------------------------------------------------- /libs/generator.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'pathname' 3 | require 'libs/coder' 4 | 5 | module Autumn 6 | 7 | # Generates the files for Autumn templates such as leaves and seasons. The 8 | # contents of these template files are populated by a {Coder} instance. 9 | 10 | class Generator 11 | # The names of the required files in a season's directory, and example for 12 | # each file. 13 | SEASON_FILES = { 14 | 'leaves.yml' => { 15 | 'Scorekeeper' => { 16 | 'class' => 'Scorekeeper' 17 | }, 18 | 'Insulter' => { 19 | 'class' => 'Insulter' 20 | }, 21 | 'Administrator' => { 22 | 'class' => 'Administrator', 23 | 'authentication' => { 24 | 'type' => 'op' 25 | } 26 | } 27 | }, 28 | 'stems.yml' => { 29 | 'Example' => { 30 | 'server' => 'irc.yourircserver.com', 31 | 'nick' => 'MyIRCBot', 32 | 'channel' => '#yourchannel', 33 | 'rejoin' => true, 34 | 'leaves' => %w(Administrator Scorekeeper Insulter) 35 | } 36 | }, 37 | 'season.yml' => { 38 | 'logging' => 'debug' 39 | }, 40 | 'database.yml' => { 41 | 'Example' => 'sqlite:path/to/example_database.db' 42 | } 43 | } 44 | 45 | # Creates a new instance. 46 | 47 | def initialize 48 | @coder = Autumn::TemplateCoder.new 49 | end 50 | 51 | # Generates the files for a new Leaf with the given name. 52 | # 53 | # @param [String] name The Leaf name. 54 | # @param [Hash] options Leaf options. 55 | # @option options [true, false] :verbose (false) If `true`, prints to 56 | # standard output every action that is taken. 57 | # @option options [:cvs, :svn, :git, nil] :vcs The version control system 58 | # used by this project. The files and directories created by this method 59 | # will be added to the project's VCS. 60 | 61 | def leaf(name, options={}) 62 | lpath = Pathname.new('leaves').join(name.snakecase) 63 | if lpath.directory? 64 | exists lpath, options 65 | return 66 | elsif lpath.exist? 67 | raise "There is a file named #{lpath} in the way." 68 | else 69 | Dir.mkdir lpath 70 | created lpath, options 71 | end 72 | 73 | cname = lpath.join('controller.rb') 74 | if cname.exist? 75 | exists cname, options 76 | else 77 | @coder.leaf(name) 78 | File.open(cname, 'w') { |file| file.puts @coder.output } 79 | created cname, options 80 | end 81 | 82 | dpath = lpath.join('data') 83 | if dpath.directory? 84 | exists dpath, options 85 | else 86 | Dir.mkdir dpath 87 | created dpath, options 88 | end 89 | 90 | gname = lpath.join('Gemfile') 91 | if gname.exist? 92 | exists gname, options 93 | else 94 | File.open(gname, 'w') { |file| file.puts "group :#{name.snakecase} do\n # Insert your leaf's gem requirements here\nend" } 95 | created gname, options 96 | end 97 | 98 | %w(lib helpers models tasks views).each do |dir| 99 | path = lpath.join('dir') 100 | if path.directory? 101 | exists path, options 102 | elsif path.exist? 103 | raise "There is a file named #{path} in the way." 104 | else 105 | Dir.mkdir path 106 | created path, options 107 | end 108 | end 109 | 110 | vname = lpath.join('views', 'about.txt.erb') 111 | if vname.exist? 112 | exists cname, options 113 | else 114 | File.open(vname, 'w') { |file| file.puts "Insert your about string here!" } 115 | created vname, options 116 | end 117 | 118 | rname = lpath.join('README') 119 | if rname.exist? 120 | exists rname, options 121 | else 122 | File.open(rname, 'w') { |file| file.puts "This is the read-me for your leaf." } 123 | created rname, options 124 | end 125 | end 126 | 127 | # Removes the files for a new leaf with the given name. 128 | # 129 | # @param [String] name The Leaf name. 130 | # @param [Hash] options Leaf options. 131 | # @option options [true, false] :verbose (false) If `true`, prints to 132 | # standard output every action that is taken. 133 | # @option options [:cvs, :svn, :git, nil] :vcs The version control system 134 | # used by this project. The files and directories created by this method 135 | # will be removed from the project's VCS. 136 | 137 | 138 | def unleaf(name, options={}) 139 | lpath = Pathname.new('leaves').join(name.snakecase) 140 | 141 | unless lpath.directory? 142 | raise "The directory #{lpath} doesn't exist." 143 | end 144 | 145 | data = lpath.join('data') 146 | if data.directory? && data.entries.size > 2 147 | print "\a" # ring the bell 148 | puts "WARNING: Files exist in this leaf's data directory!" 149 | puts "Type Ctrl-C in the next ten seconds if you don't want these files to be deleted..." 150 | (0..9).each do |num| 151 | print "#{10 - num}... " 152 | $stdout.flush 153 | sleep 1 154 | end 155 | print "\n" 156 | end 157 | 158 | FileUtils.remove_dir lpath 159 | deleted lpath, options 160 | end 161 | 162 | # Generates the files and directories for a new season with the given name. 163 | # Options: 164 | # 165 | # @param [String] name The season name. 166 | # @param [Hash] options Season options. 167 | # @option options [true, false] :verbose (false) If `true`, prints to 168 | # standard output every action that is taken. 169 | # @option see #leaf 170 | 171 | 172 | def season(name, options={}) 173 | dname = Pathname.new('config').join('seasons', name.snakecase) 174 | if dname.directory? 175 | raise "The directory #{dname} already exists." 176 | elsif dname.exist? 177 | raise "There is a file named #{dname} in the way." 178 | else 179 | Dir.mkdir dname 180 | created dname, options 181 | SEASON_FILES.each do |fname, content| 182 | fpath = dname.join(fname) 183 | if fpath.exist? 184 | exists fpath, options 185 | else 186 | File.open(fpath, 'w') { |file| file.puts content.to_yaml } 187 | created fpath, options 188 | end 189 | end 190 | end 191 | end 192 | 193 | # Removes the files and directories for a season with the given name. 194 | # Options: 195 | # 196 | # @param [String] name The season name. 197 | # @param [Hash] options Season options. 198 | # @option options [true, false] :verbose (false) If `true`, prints to 199 | # standard output every action that is taken. 200 | # @option see #unleaf 201 | 202 | def unseason(name, options={}) 203 | dname = Pathname.new('config').join('seasons', name.snakecase) 204 | unless dname.directory? 205 | raise "The directory #{dname} doesn't exist." 206 | end 207 | 208 | FileUtils.remove_dir dname 209 | deleted dname, options 210 | end 211 | 212 | private 213 | 214 | def created(path, options) 215 | puts "-- created #{path}" if options[:verbose] 216 | system 'cvs', 'add', path if options[:vcs] == :cvs 217 | system 'svn', 'add', path if options[:vcs] == :svn 218 | system 'git', 'add', path if options[:vcs] == :git 219 | end 220 | 221 | def exists(path, options) 222 | puts "-- exists #{path}" if options[:verbose] 223 | system 'cvs', 'add', path if options[:vcs] == :cvs 224 | system 'svn', 'add', path if options[:vcs] == :svn 225 | system 'git', 'add', path if options[:vcs] == :git 226 | end 227 | 228 | def deleted(path, options) 229 | puts "-- deleted #{path}" if options[:verbose] 230 | system 'cvs', 'remove', path if options[:vcs] == :cvs 231 | system 'svn', 'del', '--force', path if options[:vcs] == :svn 232 | system 'git', 'rm', '-r', path if options[:vcs] == :git 233 | end 234 | 235 | def notempty(path, options) 236 | puts "-- notempty #{path}" if options[:verbose] 237 | end 238 | 239 | def notfound(path, options) 240 | puts "-- notfound #{path}" if options[:verbose] 241 | end 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /libs/genesis.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | Autumn::Config.version = '3.0 (7-4-08)' 4 | 5 | module Autumn 6 | 7 | # Oversight class responsible for initializing the Autumn environment. To boot 8 | # the Autumn environment start all configured leaves, you make an instance of 9 | # this class and run the {#boot!} method. Leaves will each run in their own 10 | # thread, monitored by an oversight thread spawned by this class. 11 | 12 | class Genesis 13 | # @return [Speciator] The Speciator singleton. 14 | attr_reader :config 15 | 16 | # Creates a new instance that can be used to boot Autumn. 17 | 18 | def initialize 19 | load_pre_config_files 20 | @config = Speciator.instance 21 | end 22 | 23 | # Bootstraps the Autumn environment. 24 | # 25 | # @param [true, false] invoke If `true`, the leaves will be started, each in 26 | # their own thread. Use `false` for console environments. 27 | 28 | def boot!(invoke=true) 29 | load_global_settings 30 | load_post_config_files 31 | load_season_settings 32 | load_libraries 33 | init_system_logger 34 | load_daemon_info 35 | load_shared_code 36 | load_databases 37 | invoke_foliater(invoke) 38 | end 39 | 40 | # Loads the settings in the `global.yml` file. 41 | # 42 | # **Prereqs**: None 43 | 44 | def load_global_settings 45 | begin 46 | config.global YAML.load_file(Autumn::Config.root.join('config', 'global.yml')) 47 | rescue SystemCallError 48 | raise "Couldn't find your global.yml file." 49 | end 50 | config.global root: Autumn::Config.root 51 | config.global season: ENV['SEASON'] if ENV['SEASON'] 52 | end 53 | 54 | # Loads the files and gems that do not require an instantiated Speciator. 55 | # 56 | # **Prereqs**: None 57 | 58 | def load_pre_config_files 59 | require 'singleton' 60 | 61 | require 'rubygems' 62 | require 'bundler' 63 | Dir.chdir Autumn::Config.root 64 | Bundler.require :pre_config 65 | 66 | require 'facets/pathname' 67 | require 'active_support/dependencies/autoload' 68 | require 'active_support/core_ext/numeric' 69 | 70 | require 'libs/misc' 71 | require 'libs/speciator' 72 | end 73 | 74 | # Loads the files and gems that require an instantiated Speciator. 75 | # 76 | # **Prereqs**: load_global_settings 77 | 78 | def load_post_config_files 79 | require 'set' 80 | require 'yaml' 81 | require 'logger' 82 | require 'time' 83 | require 'timeout' 84 | require 'erb' 85 | require 'thread' 86 | require 'socket' 87 | require 'openssl' 88 | 89 | Bundler.require :default, config.global(:season).to_sym 90 | 91 | require 'libs/authentication' 92 | require 'libs/formatting' 93 | end 94 | 95 | # Loads the settings for the current season in its `season.yml` file. 96 | # 97 | # **Prereqs**: {#load_global_settings} 98 | 99 | def load_season_settings 100 | @season_dir = Autumn::Config.root.join('config', 'seasons', config.global(:season)) 101 | raise "The current season doesn't have a directory." unless @season_dir.directory? 102 | begin 103 | config.season YAML.load_file(@season_dir.join('season.yml')) 104 | rescue 105 | # season.yml is optional 106 | end 107 | end 108 | 109 | # Loads Autumn library objects. 110 | # 111 | # **Prereqs**: {#load_global_settings} 112 | 113 | def load_libraries 114 | require 'libs/inheritable_attributes' 115 | require 'libs/daemon' 116 | require 'libs/stem_facade' 117 | require 'libs/ctcp' 118 | require 'libs/stem' 119 | require 'libs/leaf' 120 | require 'libs/channel_leaf' 121 | require 'libs/foliater' 122 | require 'libs/log_facade' 123 | end 124 | 125 | # Initializes the system-level logger. 126 | # 127 | # **Prereqs**: {#load_libraries} 128 | 129 | def init_system_logger 130 | config.global logfile: Logger.new(log_name, config.global(:log_history) || 10, 1024*1024) 131 | begin 132 | config.global(:logfile).level = Logger.const_get(config.season(:logging).upcase) 133 | rescue NameError 134 | puts "The level #{config.season(:logging).inspect} was not understood; the log level has been raised to INFO." 135 | config.global(:logfile).level = Logger::INFO 136 | end 137 | config.global system_logger: LogFacade.new(config.global(:logfile), 'N/A', 'System') 138 | @logger = config.global(:system_logger) 139 | end 140 | 141 | # Instantiates {Daemon Daemons} from YAML files in `resources/daemons`. The 142 | # daemons are named after their YAML files. 143 | # 144 | # **Prereqs**: {#load_libraries} 145 | 146 | def load_daemon_info 147 | Autumn::Config.root.join('resources', 'daemons').glob('*.yml').each do |yml_file| 148 | yml = YAML.load_file(yml_file) 149 | Daemon.new yml_file.basename('.yml'), yml 150 | end 151 | end 152 | 153 | # Loads Ruby code in the shared directory. 154 | # 155 | # **Prereqs**: None 156 | 157 | def load_shared_code 158 | Pathname.glob(Autumn::Config.root.join('shared', '**', '*.rb')).each { |lib| load lib } 159 | end 160 | 161 | # Creates connections to databases using the DataMapper gem. 162 | # 163 | # **Prereqs**: {#load_season_settings} 164 | 165 | def load_databases 166 | db_file = @season_dir.join('database.yml') 167 | unless db_file.exist? 168 | Autumn::Config.no_database = true 169 | return 170 | end 171 | 172 | Bundler.require :datamapper 173 | require 'libs/datamapper_hacks' 174 | 175 | dbconfig = YAML.load_file(db_file) 176 | dbconfig.rekey(&:to_sym).each do |db, config| 177 | DataMapper.setup(db, config.kind_of?(Hash) ? config.rekey(&:to_sym) : config) 178 | end 179 | end 180 | 181 | # Invokes the {Foliater#load} method. Spawns a new thread to oversee the 182 | # stems' threads. This thread will exit when all leaves have terminated. 183 | # 184 | # **Prereqs**: {#load_databases}, {#load_season_settings}, 185 | # {#load_libraries}, {#init_system_logger} 186 | # 187 | # @param (see #initialize) 188 | 189 | def invoke_foliater(invoke=true) 190 | begin 191 | begin 192 | stem_config = YAML.load_file(@season_dir.join('stems.yml')) 193 | rescue Errno::ENOENT 194 | raise "Couldn't find stems.yml file for season #{config.global :season}" 195 | end 196 | begin 197 | leaf_config = YAML.load_file(@season_dir.join('leaves.yml')) 198 | rescue Errno::ENOENT 199 | # build a default leaf config 200 | leaf_config = Hash.new 201 | Autumn::Config.root.join('leaves').glob('*').each do |dir| 202 | next if !dir.directory? 203 | leaf_name = dir.basename.camelcase(:upper) 204 | leaf_config[leaf_name] = { 'class' => leaf_name } 205 | end 206 | end 207 | 208 | Foliater.instance.load stem_config, leaf_config, invoke 209 | if invoke 210 | # suspend execution of the master thread until all stems are dead 211 | while Foliater.instance.alive? 212 | Thread.stop 213 | end 214 | end 215 | rescue 216 | @logger.fatal $! 217 | end 218 | end 219 | 220 | private 221 | 222 | def log_name 223 | Autumn::Config.root.join('log', config.global(:season) + '.log') 224 | end 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /libs/inheritable_attributes.rb: -------------------------------------------------------------------------------- 1 | # This source file, originating from Ruby on Rails, extends the `Class` class to 2 | # allows attributes to be shared within an inheritance hierarchy, but where each 3 | # descendant gets a copy of their parents' attributes, instead of just a pointer 4 | # to the same. This means that the child can add elements to, for example, an 5 | # array without those additions being shared with either their parent, siblings, 6 | # or children, which is unlike the regular class-level attributes that are 7 | # shared across the entire hierarchy. 8 | # 9 | # This functionality is used by Leaf's filter features; if not for this 10 | # extension, then when a subclass changed its filter chain, all of its 11 | # superclasses' filter chains would change as well. This class allows a subclass 12 | # to inherit a _copy_ of the superclass's filter chain, but independently change 13 | # that copy without affecting the superclass's filter chain. 14 | # 15 | # Copyright (c)2004 David Heinemeier Hansson 16 | # 17 | # Permission is hereby granted, free of charge, to any person obtaining 18 | # a copy of this software and associated documentation files (the 19 | # "Software"), to deal in the Software without restriction, including 20 | # without limitation the rights to use, copy, modify, merge, publish, 21 | # distribute, sublicense, and/or sell copies of the Software, and to 22 | # permit persons to whom the Software is furnished to do so, subject to 23 | # the following conditions: 24 | # The above copyright notice and this permission notice shall be 25 | # included in all copies or substantial portions of the Software. 26 | 27 | # @private 28 | # 29 | # Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of 30 | # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements 31 | # to, for example, an array without those additions being shared with either their parent, siblings, or 32 | # children, which is unlike the regular class-level attributes that are shared across the entire hierarchy. 33 | class Class # :nodoc: 34 | def class_inheritable_reader(*syms) 35 | syms.each do |sym| 36 | next if sym.is_a?(Hash) 37 | class_eval <<-EOS 38 | def self.#{sym} 39 | read_inheritable_attribute(:#{sym}) 40 | end 41 | 42 | def #{sym} 43 | self.class.#{sym} 44 | end 45 | EOS 46 | end 47 | end 48 | 49 | def class_inheritable_writer(*syms) 50 | options = syms.last.is_a?(Hash) ? syms.pop : {} 51 | syms.each do |sym| 52 | class_eval <<-EOS 53 | def self.#{sym}=(obj) 54 | write_inheritable_attribute(:#{sym}, obj) 55 | end 56 | 57 | #{" 58 | def #{sym}=(obj) 59 | self.class.#{sym} = obj 60 | end 61 | " if options[:instance_writer] } 62 | EOS 63 | end 64 | end 65 | 66 | def class_inheritable_array_writer(*syms) 67 | options = syms.last.is_a?(Hash) ? syms.pop : {} 68 | syms.each do |sym| 69 | class_eval <<-EOS 70 | def self.#{sym}=(obj) 71 | write_inheritable_array(:#{sym}, obj) 72 | end 73 | 74 | #{" 75 | def #{sym}=(obj) 76 | self.class.#{sym} = obj 77 | end 78 | " if options[:instance_writer] } 79 | EOS 80 | end 81 | end 82 | 83 | def class_inheritable_hash_writer(*syms) 84 | options = syms.last.is_a?(Hash) ? syms.pop : {} 85 | syms.each do |sym| 86 | class_eval <<-EOS 87 | def self.#{sym}=(obj) 88 | write_inheritable_hash(:#{sym}, obj) 89 | end 90 | 91 | #{" 92 | def #{sym}=(obj) 93 | self.class.#{sym} = obj 94 | end 95 | " if options[:instance_writer] } 96 | EOS 97 | end 98 | end 99 | 100 | def class_inheritable_accessor(*syms) 101 | class_inheritable_reader(*syms) 102 | class_inheritable_writer(*syms) 103 | end 104 | 105 | def class_inheritable_array(*syms) 106 | class_inheritable_reader(*syms) 107 | class_inheritable_array_writer(*syms) 108 | end 109 | 110 | def class_inheritable_hash(*syms) 111 | class_inheritable_reader(*syms) 112 | class_inheritable_hash_writer(*syms) 113 | end 114 | 115 | def inheritable_attributes 116 | @inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES 117 | end 118 | 119 | def write_inheritable_attribute(key, value) 120 | if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES) 121 | @inheritable_attributes = {} 122 | end 123 | inheritable_attributes[key] = value 124 | end 125 | 126 | def write_inheritable_array(key, elements) 127 | write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil? 128 | write_inheritable_attribute(key, read_inheritable_attribute(key) + elements) 129 | end 130 | 131 | def write_inheritable_hash(key, hash) 132 | write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil? 133 | write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash)) 134 | end 135 | 136 | def read_inheritable_attribute(key) 137 | inheritable_attributes[key] 138 | end 139 | 140 | def reset_inheritable_attributes 141 | @inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES 142 | end 143 | 144 | private 145 | # Prevent this constant from being created multiple times 146 | EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES) 147 | 148 | def inherited_with_inheritable_attributes(child) 149 | inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes) 150 | 151 | if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES) 152 | new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES 153 | else 154 | new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)| 155 | memo.update(key => (value.dup rescue value)) 156 | end 157 | end 158 | 159 | child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes) 160 | end 161 | 162 | alias inherited_without_inheritable_attributes inherited 163 | alias inherited inherited_with_inheritable_attributes 164 | end 165 | -------------------------------------------------------------------------------- /libs/leaf.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # This is the superclass that all Autumn leaves use. To write a leaf, sublcass 4 | # this class and implement methods for each of your leaf's commands. Your 5 | # leaf's repertoire of commands is derived from the names of the methods you 6 | # write. For instance, to have your leaf respond to a `!hello` command in IRC, 7 | # write a method like so: 8 | # 9 | # ```` ruby 10 | # def hello_command(stem, sender, reply_to, msg) 11 | # stem.message "Why hello there!", reply_to 12 | # end 13 | # ```` 14 | # 15 | # You can also implement this method as: 16 | # 17 | # ```` ruby 18 | # def hello_command(stem, sender, reply_to, msg) 19 | # return "Why hello there!" 20 | # end 21 | # ```` 22 | # 23 | # Methods of the form `[word]_command` tell the leaf to respond to commands in 24 | # IRC of the form `![word]`. They should accept four parameters: 25 | # 26 | # 1. the {Stem} that received the message, 27 | # 2. the sender hash for the person who sent the message (see below), 28 | # 3. the "reply-to" string (either the name of the channel that the command 29 | # was typed on, or the nick of the person that whispered the message), and 30 | # 4. any text following the command. For instance, if the person typed "!eat A 31 | # tasty slice of pizza", the last parameter would be "A tasty slice of 32 | # pizza". This is `nil` if no text was supplied with the command. 33 | # 34 | # ### Sender hashes 35 | # 36 | # A "sender hash" is a hash with the following keys: 37 | # 38 | # | | | | 39 | # |:--------|:-------|:--------------------| 40 | # | `:nick` | String | the user's nickname | 41 | # | `:user` | String | the user's username | 42 | # | `:host` | String | the user's hostname | 43 | # 44 | # Any of these fields except `:nick` could be nil. Sender hashes are used 45 | # throughout the Stem and Leaf classes, as well as other classes; they always 46 | # have the same keys. 47 | # 48 | # ### Return values 49 | # 50 | # If your `*_command` method returns a string, it will be sent as an IRC 51 | # message to the "reply-to" parameter. If your leaf needs to respond to more 52 | # complicated commands, you will have to override the 53 | # {#did_receive_channel_message} method (see below). 54 | # 55 | # If you want to separate view logic from the controller, you can use ERb to 56 | # template your views. See the render method for more information. 57 | # 58 | # ### Removing commands 59 | # 60 | # If you like, you can remove the `quit_command` method in your subclass, for 61 | # instance, to prevent the leaf from responding to `!quit`. You can also 62 | # protect that method using filters (see "Filters"). 63 | # 64 | # Hook Methods 65 | # ------------ 66 | # 67 | # Aside from adding your own `*_command`-type methods, you should investigate 68 | # overriding the "hook" methods, such as {#will_start_up}, {#did_start_up}, 69 | # {#did_receive_private_message}, {#did_receive_channel_message}, etc. There's 70 | # a laundry list of so-named methods you can override. Their default 71 | # implementations do nothing, so there's no need to call `super`. 72 | # 73 | # Stem Convenience Methods 74 | # ------------------------ 75 | # 76 | # Most of the IRC actions (such as joining and leaving a channel, setting a 77 | # topic, etc.) are part of a {Stem} object. If your leaf is only running off 78 | # of one stem, you can call these stem methods directly, as if they were 79 | # methods in the Leaf class. Otherwise, you will need to specify which stem 80 | # to perform these IRC actions on. Usually, the stem is given to you, as a 81 | # parameter for your `*_command` method, for instance. 82 | # 83 | # For the sake of convenience, you can make Stem method calls on the `stems` 84 | # attribute; these calls will be forwarded to every stem in the `stems` 85 | # attribute. For instance, to broadcast a message to all servers and all 86 | # channels: 87 | # 88 | # ```` ruby 89 | # stems.message "Ready for orders!" 90 | # ```` 91 | # 92 | # Filters 93 | # ------- 94 | # 95 | # Like Ruby on Rails, you can add filters to each of your commands to be 96 | # executed before or after the command is run. You can do this using the 97 | # {.before_filter} and {.after_filter} methods, just like in Rails. Filters 98 | # are run in the order they are added to the chain. Thus, if you wanted to run 99 | # your preload filter before you ran your cache filter, you'd write the calls 100 | # in this order: 101 | # 102 | # ```` ruby 103 | # class MyLeaf < Leaf 104 | # before_filter :my_preload 105 | # before_filter :my_cache 106 | # end 107 | # ```` 108 | # 109 | # See the documentation for the {.before_filter} and {.after_filter} methods 110 | # and the README file for more information on filters. 111 | # 112 | # Authentication 113 | # -------------- 114 | # 115 | # If a leaf is initialized with a hash for the `authentication` option, the 116 | # values of that hash are used to choose an authenticator that will be run 117 | # before each command. This authenticator will determine whether or not the 118 | # user can run that command. The options that can be specified in this hash 119 | # are: 120 | # 121 | # | | | | 122 | # |:----------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 123 | # | `:type` | String | The name of a class in the {Autumn::Authentication} module, in snake_case. Thus, if you wanted to use the {Autumn::Authentication::Password} class, which does password-based authentication, you'd set this value to `password`. | 124 | # | `:only` | Array | A list of protected commands for which authentication is required; all other commands are unprotected. | 125 | # | `:except` | Array | A list of unprotected commands; all other commands require authentication. | 126 | # | `:silent` | true, false | Normally, when someone fails to authenticate himself before running a protected command, the leaf responds with an error message (e.g., "You have to authenticate with a password first"). Set this to `true` to suppress this behavior. | 127 | # 128 | # In addition, you can also specify any custom options for your authenticator. 129 | # These options are passed to the authenticator's `initialize` method. See the 130 | # classes in the Autumn::Authentication module for such options. 131 | # 132 | # If you annotate a command method as protected, the authenticator will be run 133 | # unconditionally, regardless of the `:only` or `:except` options: 134 | # 135 | # ```` ruby 136 | # class Controller < Autumn::Leaf 137 | # def destructive_command(stem, sender, reply_to, msg) 138 | # # ... 139 | # end 140 | # ann :destructive_command, protected: true 141 | # end 142 | # ```` 143 | # 144 | # Logging 145 | # ------- 146 | # 147 | # Autumn comes with a framework for logging as well. It's very similar to the 148 | # Ruby on Rails logging framework. To log an error message: 149 | # 150 | # ```` ruby 151 | # logger.error "Quiz data is missing!" 152 | # ```` 153 | # 154 | # By default the logger will only log `info` events and above in production 155 | # seasons, and will log all messages for debug seasons. (See the README for 156 | # more on seasons.) To customize the logger, and for more information on 157 | # logging, see the {LogFacade} class documentation. 158 | # 159 | # Colorizing and Formatting Text 160 | # ------------------------------ 161 | # 162 | # The {Autumn::Formatting} module contains sub-modules which handle formatting 163 | # for different clients (such as mIRC-style formatting, the most common). The 164 | # specific formatting module that's included depends on the leaf's 165 | # initialization options; see {#initialize}. 166 | 167 | class Leaf 168 | extend Anise::Annotations 169 | 170 | # Default for the `command_prefix` init option. 171 | DEFAULT_COMMAND_PREFIX = '!' 172 | @@view_alias = Hash.new { |h, k| k } 173 | 174 | # @return [LogFacade] The logger instance for this leaf. 175 | attr :logger 176 | # @return [Array] The Stem instances running this leaf. 177 | attr :stems 178 | # @return [Hash] The configuration for this leaf. 179 | attr :options 180 | 181 | # Instantiates a leaf. This is generally handled by the {Foliater} class. 182 | # 183 | # @param [Hash] opts Initialization options, as well as any user-defined 184 | # options you wish. 185 | # @option options [String] :command_prefix (DEFAULT_COMMAND_PREFIX) The 186 | # string that must precede all command names. 187 | # @option options [true, false] :responds_to_private_messages If `true`, the 188 | # bot responds to known commands sent in private messages. 189 | # @option options [LogFacade] :logger The logger instance for this leaf. 190 | # @option options [String] :database The name of a custom database 191 | # connection to use. 192 | # @option options [String] :formatter (Autumn::Formatting::DEFAULT) The name 193 | # of an Autumn::Formatting class to use as the formatted. 194 | # @option options [Hash] :authentication Additional authentication options 195 | # (see class documentation). 196 | 197 | def initialize(opts={}) 198 | @port = opts[:port] 199 | @options = opts 200 | @options[:command_prefix] ||= DEFAULT_COMMAND_PREFIX 201 | @break_flag = false 202 | @logger = options[:logger] 203 | 204 | @stems = Set.new 205 | # Let the stems array respond to methods as if it were a single stem 206 | class << @stems 207 | def method_missing(meth, *args) 208 | if all? { |stem| stem.respond_to? meth } 209 | collect { |stem| stem.send(meth, *args) } 210 | else 211 | super 212 | end 213 | end 214 | end 215 | end 216 | 217 | # @private 218 | def preconfigure 219 | if options[:authentication] 220 | @authenticator = Autumn::Authentication.const_get(options[:authentication]['type'].camelcase(:upper)).new(options[:authentication].rekey(&:to_sym)) 221 | stems.add_listener @authenticator 222 | end 223 | end 224 | 225 | # @private Simplifies method calls for one-stem leaves. 226 | def method_missing(meth, *args) # :nodoc: 227 | if stems.size == 1 && stems.only.respond_to?(meth) 228 | stems.only.send meth, *args 229 | else 230 | super 231 | end 232 | end 233 | 234 | ########################## METHODS INVOKED BY STEM ######################### 235 | 236 | # @private 237 | def stem_ready(_) 238 | return unless (@ready_mutex ||= Mutex.new).synchronize { stems.ready?.all? } 239 | database { startup_check } 240 | end 241 | 242 | # @private 243 | def irc_privmsg_event(stem, sender, arguments) 244 | database do 245 | if arguments[:channel] 246 | command_parse stem, sender, arguments 247 | did_receive_channel_message stem, sender, arguments[:channel], arguments[:message] 248 | else 249 | command_parse stem, sender, arguments if options[:respond_to_private_messages] 250 | did_receive_private_message stem, sender, arguments[:message] 251 | end 252 | end 253 | end 254 | 255 | # @private 256 | def irc_join_event(stem, sender, arguments) 257 | database { someone_did_join_channel stem, sender, arguments[:channel] } 258 | end 259 | 260 | # @private 261 | def irc_part_event(stem, sender, arguments) 262 | database { someone_did_leave_channel stem, sender, arguments[:channel] } 263 | end 264 | 265 | # @private 266 | def irc_mode_event(stem, sender, arguments) 267 | database do 268 | if arguments[:recipient] 269 | gained_usermodes(stem, arguments[:mode]) { |prop| someone_did_gain_usermode stem, arguments[:recipient], prop, arguments[:parameter], sender } 270 | lost_usermodes(stem, arguments[:mode]) { |prop| someone_did_lose_usermode stem, arguments[:recipient], prop, arguments[:parameter], sender } 271 | elsif arguments[:parameter] && stem.server_type.privilege_mode?(arguments[:mode]) 272 | gained_privileges(stem, arguments[:mode]) { |prop| someone_did_gain_privilege stem, arguments[:channel], arguments[:parameter], prop, sender } 273 | lost_privileges(stem, arguments[:mode]) { |prop| someone_did_lose_privilege stem, arguments[:channel], arguments[:parameter], prop, sender } 274 | else 275 | gained_properties(stem, arguments[:mode]) { |prop| channel_did_gain_property stem, arguments[:channel], prop, arguments[:parameter], sender } 276 | lost_properties(stem, arguments[:mode]) { |prop| channel_did_lose_property stem, arguments[:channel], prop, arguments[:parameter], sender } 277 | end 278 | end 279 | end 280 | 281 | # @private 282 | def irc_topic_event(stem, sender, arguments) 283 | database { someone_did_change_topic stem, sender, arguments[:channel], arguments[:topic] } 284 | end 285 | 286 | # @private 287 | def irc_invite_event(stem, sender, arguments) 288 | database { someone_did_invite stem, sender, arguments[:recipient], arguments[:channel] } 289 | end 290 | 291 | # @private 292 | def irc_kick_event(stem, sender, arguments) 293 | database { someone_did_kick stem, sender, arguments[:channel], arguments[:recipient], arguments[:message] } 294 | end 295 | 296 | # @private 297 | def irc_notice_event(stem, sender, arguments) 298 | database do 299 | if arguments[:recipient] 300 | did_receive_notice stem, sender, arguments[:recipient], arguments[:message] 301 | else 302 | did_receive_notice stem, sender, arguments[:channel], arguments[:message] 303 | end 304 | end 305 | end 306 | 307 | # @private 308 | def irc_nick_event(stem, sender, arguments) 309 | database { nick_did_change stem, sender, arguments[:nick] } 310 | end 311 | 312 | # @private 313 | def irc_quit_event(stem, sender, arguments) 314 | database { someone_did_quit stem, sender, arguments[:message] } 315 | end 316 | 317 | ########################### OTHER PUBLIC METHODS ########################### 318 | 319 | # Invoked just before the leaf starts up. Override this method to do any 320 | # pre-startup tasks you need. The leaf is fully initialized and all methods 321 | # and helper objects are available. 322 | 323 | def will_start_up 324 | end 325 | 326 | # Performs the block in the context of a database, referenced by symbol. For 327 | # instance, if you had defined in `database.yml` a connection named 328 | # "scorekeeper", you could access that connection like so: 329 | # 330 | # ```` ruby 331 | # database(:scorekeeper) do 332 | # # [...] 333 | # end 334 | # ```` 335 | # 336 | # If your database is named after your leaf (as in the example above for a 337 | # leaf named "Scorekeeper"), it will automatically be set as the database 338 | # context for the scope of all hook, filter and command methods. However, if 339 | # your database connection is named differently, or if you are working in a 340 | # method not invoked by the Leaf class, you will need to set the connection 341 | # using this method. 342 | # 343 | # If you omit the `dbname` parameter, it will try to guess the name of your 344 | # database connection using the leaf's name and the leaf's class name. 345 | # 346 | # If the database connection cannot be found, the block is executed with no 347 | # database scope. 348 | # 349 | # @param [String] dbname The name of the database connection. 350 | # @yield A block to execute in the context of the database connection. 351 | 352 | def database(dbname=nil, &block) 353 | dbname ||= database_name 354 | if dbname 355 | repository dbname, &block 356 | else 357 | yield 358 | end 359 | end 360 | 361 | # @private 362 | # 363 | # Tries to guess the name of the database connection this leaf is using. 364 | # Looks for database connections named after either this leaf's identifier 365 | # or this leaf's class name. Returns nil if no suitable connection is found. 366 | 367 | def database_name 368 | return nil unless Module.constants.include?('DataMapper') || Module.constants.include?(:DataMapper) 369 | raise "No such database connection #{options[:database]}" if options[:database] && DataMapper::Repository.adapters[options[:database]].nil? 370 | # Custom database connection specified 371 | return options[:database].to_sym if options[:database] 372 | # Leaf config name 373 | return leaf_name.to_sym if DataMapper::Repository.adapters[leaf_name.to_sym] 374 | # Leaf config name, underscored 375 | return leaf_name.methodize.to_sym if DataMapper::Repository.adapters[leaf_name.methodize.to_sym] 376 | # Leaf class name 377 | return self.class.to_s.to_sym if DataMapper::Repository.adapters[self.class.to_s.to_sym] 378 | # Leaf class name, underscored 379 | return self.class.to_s.methodize.to_sym if DataMapper::Repository.adapters[self.class.to_s.methodize.to_sym] 380 | # I give up 381 | return nil 382 | end 383 | 384 | # @private 385 | def inspect 386 | "#<#{self.class.to_s} #{leaf_name}>" 387 | end 388 | 389 | protected 390 | 391 | # Duplicates a command. This method aliases the command method and also 392 | # ensures the correct view file is rendered if appropriate. 393 | # 394 | # @param [Symbol] old The original command name. 395 | # @param [Symbol] new The new command name. 396 | # 397 | # @example 398 | # alias_command :google, :g 399 | 400 | def self.alias_command(old, new) 401 | raise NoMethodError, "Unknown command #{old}" unless instance_methods.include?(:"#{old}_command") 402 | alias_method :"#{new}_command", :"#{old}_command" 403 | @@view_alias[new] = old 404 | end 405 | 406 | # Adds a filter to the end of the list of filters to be run before a command 407 | # is executed. You can use these filters to perform tasks that prepare the 408 | # leaf to respond to a command, or to determine whether or not a command 409 | # should be run (e.g., authentication). 410 | # 411 | # Your method will be called with these parameters: 412 | # 413 | # 1. the {Stem} instance that received the command, 414 | # 2. the name of the channel to which the command was sent (or `nil` if it 415 | # was a private message), 416 | # 3. the sender hash, 417 | # 4. the name of the command that was typed, as a symbol, 418 | # 5. any additional parameters after the command (same as the `msg` 419 | # parameter in the `*_command` methods), and 420 | # 6. the custom options that were given to `before_filter`. 421 | # 422 | # If your filter returns either `nil` or `false`, the filter chain will be 423 | # halted and the command will not be run. For example, if you create the 424 | # filter: 425 | # 426 | # ```` ruby 427 | # before_filter :read_files, only: [ :quit, :reload ], remote_files: true 428 | # ```` 429 | # 430 | # then any time the bot receives a "!quit" or "!reload" command, it will 431 | # first evaluate: 432 | # 433 | # ```` ruby 434 | # read_files_filter , , , , , { remote_files: true } 435 | # ```` 436 | # 437 | # and if the result is not `false` or `nil`, the command will be executed. 438 | # 439 | # @param [Symbol] filter The name of the filter method, with `_filter` 440 | # removed. 441 | # @param [Hash] options Additional options. 442 | # @option options [Symbol, Array] :only If set, the filter will only 443 | # be run on these commands (omit `_command`). 444 | # @option options [Symbol, Array] :except If set, the filter will 445 | # only be run on other than these commands (omit `_command`). 446 | 447 | def self.before_filter(filter, options={}) 448 | if options[:only] && !options[:only].kind_of?(Array) 449 | options[:only] = [options[:only]] 450 | end 451 | if options[:except] && !options[:except].kind_of?(Array) 452 | options[:except] = [options[:except]] 453 | end 454 | write_inheritable_array 'before_filters', [[filter.to_sym, options]] 455 | end 456 | 457 | # Adds a filter to the end of the list of filters to be run after a command 458 | # is executed. You can use these filters to perform tasks that must be done 459 | # after a command is run, such as cleaning up temporary files. Pass the name 460 | # of your filter as a symbol, and an optional has of options. See the 461 | # before_filter docs for more. 462 | # 463 | # Your method will be called with five parameters -- see the before_filter 464 | # method for more information. Unlike before_filter filters, however, any 465 | # return value is ignored. For example, if you create the filter: 466 | # 467 | # after_filter :clean_tmp, only: :sendfile, remove_symlinks: true 468 | # 469 | # then any time the bot receives a "!sendfile" command, after running the 470 | # command it will evaluate: 471 | # 472 | # clean_tmp_filter , , , :sendfile, , { remove_symlinks: true } 473 | # 474 | # @param (see #before_filter) 475 | 476 | def self.after_filter(filter, options={}) 477 | if options[:only] && !options[:only].kind_of?(Array) 478 | options[:only] = [options[:only]] 479 | end 480 | if options[:except] && !options[:except].kind_of?(Array) 481 | options[:except] = [options[:except]] 482 | end 483 | write_inheritable_array 'after_filters', [[filter.to_sym, options]] 484 | end 485 | 486 | # Invoked after the leaf is started up and is ready to accept commands. 487 | # Override this method to do any post-startup tasks you need, such as 488 | # displaying a greeting message. 489 | 490 | def did_start_up 491 | end 492 | 493 | # Invoked just before the leaf exists. Override this method to perform any 494 | # pre-shutdown tasks you need. 495 | 496 | def will_quit 497 | end 498 | 499 | # Invoked when the leaf receives a private (whispered) message. 500 | # 501 | # @param [Stem] stem The stem on which the private message was received. 502 | # @param [Hash] sender The sender hash. 503 | # @param [String] msg The private message content. 504 | 505 | def did_receive_private_message(stem, sender, msg) 506 | end 507 | 508 | # Invoked when a message is sent to a channel the leaf is a member of (even 509 | # if that message was a valid command). 510 | # 511 | # @param [Stem] stem The stem on which the message was received. 512 | # @param [Hash] sender The sender hash. 513 | # @param [String] channel The name of the channel the message was sent to. 514 | # @param [String] msg The message content. 515 | 516 | def did_receive_channel_message(stem, sender, channel, msg) 517 | end 518 | 519 | # Invoked when someone joins a channel the leaf is a member of. 520 | # 521 | # @param [Stem] stem The stem with the channel. 522 | # @param [Hash] person The person that joined the channel (sender hash). 523 | # @param [String] channel The channel the person joined. 524 | 525 | def someone_did_join_channel(stem, person, channel) 526 | end 527 | 528 | # Invoked when someone leaves a channel the leaf is a member of. 529 | # 530 | # @param [Stem] stem The stem with the channel. 531 | # @param [Hash] person The person that left the channel (sender hash). 532 | # @param [String] channel The channel the person left. 533 | 534 | def someone_did_leave_channel(stem, person, channel) 535 | end 536 | 537 | # Invoked when someone gains a channel privilege. 538 | # 539 | # @param [Stem] stem The stem on which the event occurred. 540 | # @param [String] channel The channel in which the privilege was granted. 541 | # @param [String] nick The nickname of the person who received the 542 | # privilege. 543 | # @param [Symbol, String] privilege The privilege that was granted. It will 544 | # be a symbol (as defined in the {Daemon}, such as `:voice`), or a string 545 | # if the Daemon does not define the privilege (such as "v" for voice). 546 | # @param [Hash] bestower The person who bestowed the privilege (sender 547 | # hash). 548 | 549 | def someone_did_gain_privilege(stem, channel, nick, privilege, bestower) 550 | end 551 | 552 | # Invoked when someone loses a channel privilege. 553 | # 554 | # @param [Stem] stem The stem on which the event occurred. 555 | # @param [String] channel The channel in which the privilege was revoked. 556 | # @param [String] nick The nickname of the person who lost the privilege. 557 | # @param [Symbol, String] privilege The privilege that was revoked. It will 558 | # be a symbol (as defined in the {Daemon}, such as `:voice`), or a string 559 | # if the Daemon does not define the privilege (such as "v" for voice). 560 | # @param [Hash] bestower The person who revoked the privilege (sender hash). 561 | 562 | def someone_did_lose_privilege(stem, channel, nick, privilege, bestower) 563 | end 564 | 565 | # Invoked when a channel gains a property. 566 | # 567 | # @param [Stem] stem The stem on which the event occurred. 568 | # @param [String] channel The channel that gained the property. 569 | # @param [Symbol, String] property The property that was set. It will be a 570 | # symbol (as defined in the {Daemon}, such as `:secret`), or a string if 571 | # the Daemon does not define the privilege (such as "s" for secret). 572 | # @param [String] argument An additional argument for the mode, if provided. 573 | # @param [Hash] bestower The person who set the channel property (sender 574 | # hash). 575 | 576 | def channel_did_gain_property(stem, channel, property, argument, bestower) 577 | end 578 | 579 | # Invoked when a channel loses a property. 580 | # 581 | # @param [Stem] stem The stem on which the event occurred. 582 | # @param [String] channel The channel that lost the property. 583 | # @param [Symbol, String] property The property that was removed. It will be 584 | # a symbol (as defined in the {Daemon}, such as `:secret`), or a string if 585 | # the Daemon does not define the privilege (such as "s" for secret). 586 | # @param [String] argument An additional argument for the mode, if provided. 587 | # @param [Hash] bestower The person who removed the channel property (sender 588 | # hash). 589 | 590 | def channel_did_lose_property(stem, channel, property, argument, bestower) 591 | end 592 | 593 | # Invoked when someone sets a usermode on a nick. 594 | # 595 | # @param [Stem] stem The stem on which the event occurred. 596 | # @param [String] nick The nickname of the person who gained the usermode. 597 | # @param [Symbol, String] mode The mode that was set. It will be a symbol 598 | # (as defined in the {Daemon}, such as `:invisible`), or a string if the 599 | # Daemon does not define the privilege (such as "i" for invisible). 600 | # @param [String] argument An additional argument for the mode, if provided. 601 | # @param [Hash] bestower The person who set the property (sender hash). 602 | 603 | def someone_did_gain_usermode(stem, nick, mode, argument, bestower) 604 | end 605 | 606 | # Invoked when someone removes a usermode on a nick. 607 | # 608 | # @param [Stem] stem The stem on which the event occurred. 609 | # @param [String] nick The nickname of the person who lost the usermode. 610 | # @param [Symbol, String] mode The mode that was removed. It will be a 611 | # symbol (as defined in the {Daemon}, such as `:invisible`), or a string 612 | # if the Daemon does not define the privilege (such as "i" for invisible). 613 | # @param [String] argument An additional argument for the mode, if provided. 614 | # @param [Hash] bestower The person who removed the property (sender hash). 615 | 616 | def someone_did_lose_usermode(stem, nick, mode, argument, bestower) 617 | end 618 | 619 | # Invoked when someone changes a channel's topic. 620 | # 621 | # @param [Stem] stem The stem on which the event occurred. 622 | # @param [Hash] person The person who set the topic (sender hash). 623 | # @param [String] channel The channel who's topic was changed. 624 | # @param [String] topic The new topic message. 625 | 626 | def someone_did_change_topic(stem, person, channel, topic) 627 | end 628 | 629 | # Invoked when someone invites another person to a channel. For some IRC 630 | # servers, this will only be invoked if the leaf itself is invited into a 631 | # channel. 632 | # 633 | # @param [Stem] stem The stem on which the event occurred. 634 | # @param [Hash] inviter The person who sent the invite (sender hash). 635 | # @param [String] invitee The person who received the invite (nickname). 636 | # @param [String] channel The channel the person was invited into. 637 | 638 | def someone_did_invite(stem, inviter, invitee, channel) 639 | end 640 | 641 | # Invoked when someone is kicked from a channel. Note that this is called 642 | # when your leaf is kicked as well, so it may well be the case that 643 | # `channel` is a channel you are no longer in! 644 | # 645 | # @param [Stem] stem The stem on which the event occurred. 646 | # @param [Hash] kicker The person who gave the boot (sender hash). 647 | # @param [String] channel The channel the person was kicked from. 648 | # @param [String] victim The person who got the boot (nickname). 649 | # @param [String] msg The message accompanying the kick. 650 | 651 | def someone_did_kick(stem, kicker, channel, victim, msg) 652 | end 653 | 654 | # Invoked when a notice is received. Notices are like channel or private 655 | # messages, except that leaves are expected _not_ to respond to them. 656 | # 657 | # @param [Stem] stem The stem on which the event occurred. 658 | # @param [Hash] sender The person who sent the notice (sender hash). 659 | # @param [String] recipient The channel or nickname that received the 660 | # notice. 661 | # @param [String] msg The notice message. 662 | 663 | def did_receive_notice(stem, sender, recipient, msg) 664 | end 665 | 666 | # Invoked when a user changes his nick. 667 | # 668 | # @param [Stem] stem The stem on which the event occurred. 669 | # @param [Hash] person The person who changed their nick (including his/her 670 | # old nick). 671 | # @param [String] nick The person's new nick. 672 | 673 | def nick_did_change(stem, person, nick) 674 | end 675 | 676 | # Invoked when someone quits IRC. 677 | # 678 | # @param [Stem] stem The stem on which the event occurred. 679 | # @param [Hash] person The person who quit (sender hash). 680 | # @param [String] msg The quit message. 681 | 682 | def someone_did_quit(stem, person, msg) 683 | end 684 | 685 | # @private 686 | UNADVERTISED_COMMANDS = %w(about commands) 687 | 688 | # Typing this command displays a list of all commands for each leaf running 689 | # off this stem. 690 | 691 | def commands_command(stem, sender, reply_to, msg) 692 | commands = self.class.instance_methods.select { |m| m =~ /^\w+_command$/ } 693 | commands.map! { |m| m.to_s.match(/^(\w+)_command$/)[1] } 694 | commands.reject! { |m| UNADVERTISED_COMMANDS.include? m } 695 | return if commands.empty? 696 | commands.map! { |c| "#{options[:command_prefix]}#{c}" } 697 | "Commands for #{leaf_name}: #{commands.sort.join(', ')}" 698 | end 699 | 700 | # Sets a custom view name to render. The name doesn't have to correspond to 701 | # an actual command, just an existing view file. Example: 702 | # 703 | # ```` ruby 704 | # def my_command(stem, sender, reply_to, msg) 705 | # render :help and return if msg.empty? # user doesn't know how to use the command 706 | # # [...] 707 | # end 708 | # ```` 709 | # 710 | # Only one view is rendered per command. If this method is called multiple 711 | # times, an exception is raised. This method has no effect outside of a 712 | # `*_command` method. 713 | # 714 | # By default, the view named after the command will be rendered. If no such 715 | # view exists, the value returned by the method will be used as the 716 | # response. 717 | # 718 | # @param [Symbol] view A view name. 719 | # @raise [StandardError] If called more than once per command. 720 | 721 | def render(view) 722 | # Since only one command is executed per thread, we can store the view to 723 | # render as a thread-local variable. 724 | raise "The render method should be called at most once per command" if Thread.current[:render_view] 725 | Thread.current[:render_view] = view.to_s 726 | return nil 727 | end 728 | 729 | # Gets or sets a variable for use in the view. Use this method in 730 | # `*_command` methods to pass data to the view ERb file, and in the ERb file 731 | # to retrieve these values. For example, in your `controller.rb` file: 732 | # 733 | # ```` ruby 734 | # def my_command(stem, sender, reply_to, msg) 735 | # var num_lights: 4 736 | # end 737 | # ```` 738 | # 739 | # And in your `my.txt.erb` file: 740 | # 741 | # ```` erb 742 | # THERE ARE <%= var :num_lights %> LIGHTS! 743 | # ```` 744 | # 745 | # @overload vars(var) 746 | # Retrieves a stored value. 747 | # @param [Symbol] var The value name. 748 | # @return The value. 749 | # 750 | # @overload vars(values) 751 | # @param [Hash] values A hash mapping var names to the 752 | # values to set for those vars. 753 | 754 | def var(vars) 755 | return Thread.current[:vars][vars] if vars.kind_of? Symbol 756 | return vars.each { |var, val| Thread.current[:vars][var] = val } if vars.kind_of? Hash 757 | raise ArgumentError, "var must take a symbol or a hash" 758 | end 759 | 760 | private 761 | 762 | def startup_check 763 | return if @started_up 764 | @started_up = true 765 | did_start_up 766 | end 767 | 768 | def command_parse(stem, sender, arguments) 769 | if arguments[:channel] || options[:respond_to_private_messages] 770 | reply_to = arguments[:channel] ? arguments[:channel] : sender[:nick] 771 | matches = arguments[:message].match(/^#{Regexp.escape options[:command_prefix]}(\w+)\s*(.*)$/) 772 | if matches 773 | name = matches[1].to_sym 774 | msg = matches[2] 775 | command_exec name, stem, arguments[:channel], sender, msg, reply_to 776 | end 777 | end 778 | end 779 | 780 | def command_exec(name, stem, channel, sender, msg, reply_to) 781 | cmd_sym = "#{name}_command".to_sym 782 | return unless respond_to? cmd_sym 783 | msg = nil if msg.presence 784 | 785 | return unless authenticated?(name, stem, channel, sender) 786 | return unless run_before_filters(name, stem, channel, sender, name, msg) 787 | 788 | Thread.current[:vars] = Hash.new 789 | return_val = send(cmd_sym, stem, sender, reply_to, msg) 790 | view = Thread.current[:render_view] 791 | view ||= @@view_alias[name] 792 | if return_val.kind_of? String 793 | stem.message return_val, reply_to 794 | elsif options[:views][view.to_s] 795 | stem.message parse_view(view.to_s), reply_to 796 | #else 797 | # raise "You must either specify a view to render or return a string to send." 798 | end 799 | Thread.current[:vars] = nil 800 | Thread.current[:render_view] = nil # Clear it out in case the command is synchronized 801 | run_after_filters name, stem, channel, sender, name, msg 802 | end 803 | 804 | def parse_view(name) 805 | return nil unless options[:views][name] 806 | ERB.new(options[:views][name]).result(binding) 807 | end 808 | 809 | def leaf_name 810 | Foliater.instance.leaves.key self 811 | end 812 | 813 | def run_before_filters(cmd, stem, channel, sender, _, msg) 814 | command = cmd.to_sym 815 | self.class.before_filters.each do |filter, options| 816 | local_opts = options.dup 817 | next if local_opts[:only] && !local_opts.delete(:only).include?(command) 818 | next if local_opts[:except] && local_opts.delete(:except).include?(command) 819 | return false unless method("#{filter}_filter")[stem, channel, sender, command, msg, local_opts] 820 | end 821 | return true 822 | end 823 | 824 | def run_after_filters(cmd, stem, channel, sender, _, msg) 825 | command = cmd.to_sym 826 | self.class.after_filters.each do |filter, options| 827 | local_opts = options.dup 828 | next if local_opts[:only] && !local_opts.delete(:only).include?(command) 829 | next if local_opts[:except] && local_opts.delete(:except).include?(command) 830 | method("#{filter}_filter")[stem, channel, sender, command, msg, local_opts] 831 | end 832 | end 833 | 834 | def authenticated?(cmd, stem, channel, sender) 835 | return true if @authenticator.nil? 836 | # Any method annotated as protected is authenticated unconditionally 837 | return true unless self.class.ann("#{cmd}_command".to_sym, :protected) 838 | if @authenticator.authenticate(stem, channel, sender, self) 839 | return true 840 | else 841 | stem.message @authenticator.unauthorized, channel unless options[:authentication]['silent'] 842 | return false 843 | end 844 | end 845 | 846 | def gained_privileges(stem, privstr) 847 | return unless privstr[0, 1] == '+' 848 | privstr.except_first.each_char { |c| yield stem.server_type.privilege[c] } 849 | end 850 | 851 | def lost_privileges(stem, privstr) 852 | return unless privstr[0, 1] == '-' 853 | privstr.except_first.each_char { |c| yield stem.server_type.privilege[c] } 854 | end 855 | 856 | def gained_properties(stem, propstr) 857 | return unless propstr[0, 1] == '+' 858 | propstr.except_first.each_char { |c| yield stem.server_type.channel_mode[c] } 859 | end 860 | 861 | def lost_properties(stem, propstr) 862 | return unless propstr[0, 1] == '-' 863 | propstr.except_first.each_char { |c| yield stem.server_type.channel_mode[c] } 864 | end 865 | 866 | def gained_usermodes(stem, modestr) 867 | return unless modestr[0, 1] == '+' 868 | modestr.except_first.each_char { |c| yield stem.server_type.usermode[c] } 869 | end 870 | 871 | def lost_usermodes(stem, modestr) 872 | return unless modestr[0, 1] == '-' 873 | modestr.except_first.each_char { |c| yield stem.server_type.usermode[c] } 874 | end 875 | 876 | def self.before_filters 877 | read_inheritable_attribute('before_filters') || [] 878 | end 879 | 880 | def self.after_filters 881 | read_inheritable_attribute('after_filters') || [] 882 | end 883 | end 884 | end 885 | -------------------------------------------------------------------------------- /libs/log_facade.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # This class is a facade for Ruby's `Logger` that adds additional information 4 | # to log entries. LogFacade will pass any method calls onto a Logger instance, 5 | # but reformat log entries to include an Autumn object's type and name. 6 | # 7 | # For example, if you wanted a LogFacade for a Leaf named "Scorekeeper", you 8 | # could instantiate one: 9 | # 10 | # ```` ruby 11 | # facade = LogFacade.new(logger, 'Leaf', 'Scorekeeper') 12 | # ```` 13 | # 14 | # And a call such as: 15 | # 16 | # ```` ruby 17 | # facade.info "Starting up" 18 | # ```` 19 | # 20 | # Would be reformatted as "Scorekeeper (Leaf): Starting up". 21 | # 22 | # In addition, this class will log messages to `STDOUT` if the `debug` global 23 | # option is set. Instantiation of this class is handled by {Genesis} and 24 | # should not normally be done by the user. 25 | 26 | class LogFacade 27 | # @return [String] The Autumn object type (typically "Stem" or "Leaf"). 28 | attr :type 29 | # @return [String] The name of the Autumn object. 30 | attr :name 31 | 32 | # Creates a new facade for `logger` that prepends type and name information 33 | # to each log message. 34 | # 35 | # @param [Logger] logger A logger instance. 36 | # @param [String] type The Autumn object type (e.g., "Stem" or "Leaf"). 37 | # @param [String] name The Autumn object name (leaf or stem name). 38 | 39 | def initialize(logger, type, name) 40 | @type = type 41 | @name = name 42 | @logger = logger 43 | @stdout = Speciator.instance.season(:logging) == 'debug' 44 | end 45 | 46 | # @private 47 | def method_missing(meth, *args) 48 | if args.size == 1 && args.only.kind_of?(String) 49 | args = ["#{name} (#{type}): #{args.only}"] 50 | end 51 | @logger.send meth, *args 52 | puts (args.first.kind_of?(Exception) ? (args.first.to_s + "\n" + args.first.backtrace.join("\n")) : args.first) if @stdout 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /libs/misc.rb: -------------------------------------------------------------------------------- 1 | # Miscellaneous extra methods and objects used by Autumn, and additions to Ruby 2 | # Core objects. 3 | 4 | # @private 5 | class Numeric 6 | 7 | # Possibly pluralizes a noun based on this number's value. Returns this number 8 | # and the noun as a string. This method attempts to use the Ruby English gem 9 | # if available, and falls back on the very simple default of appending an "s" 10 | # to the word to make it plural. If the Ruby English gem is not available, you 11 | # can specify a custom plural form for the word. Examples: 12 | # 13 | # 5.pluralize('dog') #=> "5 dogs" 14 | # 1.pluralize('car') #=> "1 car" 15 | # 7.pluralize('mouse', 'mice') #=> "7 mice" (only necessary if Ruby English is not installed) 16 | 17 | def pluralize(singular, plural=nil) 18 | begin 19 | return "#{to_s} #{self == 1 ? singular : singular.plural}" 20 | rescue Gem::LoadError 21 | plural ||= singular + 's' 22 | return "#{to_s} #{(self == 1) ? singular : plural}" 23 | end 24 | end 25 | end 26 | 27 | # @private 28 | class String 29 | 30 | # Returns a copy of this string with the first character dropped. 31 | 32 | def except_first 33 | self[1, size-1] 34 | end 35 | 36 | # Removes modules from a full class name. 37 | 38 | def demodulize 39 | split("::").last 40 | end 41 | end 42 | 43 | # @private 44 | class Hash 45 | 46 | # Returns a hash that gives back the key if it has no value for that key. 47 | 48 | def self.parroting(hsh={}) 49 | hsh ||= Hash.new 50 | Hash.new { |h, k| k }.update(hsh) 51 | end 52 | end 53 | 54 | # @private 55 | # 56 | # An implementation of `SizedQueue` that, instead of blocking when the queue is 57 | # full, simply discards the overflow, forgetting it. 58 | 59 | class ForgetfulQueue < Queue 60 | 61 | # Creates a new sized queue. 62 | 63 | def initialize(capacity) 64 | super() 65 | @max = capacity 66 | @mutex = Mutex.new 67 | end 68 | 69 | # Returns true if this queue is at maximum size. 70 | 71 | def full? 72 | size == @max 73 | end 74 | 75 | # Pushes an object onto the queue. If there is no space left on the queue, 76 | # does nothing. 77 | 78 | def push(obj) 79 | super unless full? 80 | end 81 | alias_method :<<, :push 82 | alias_method :enq, :push 83 | end 84 | 85 | # @private Adds the only method to Set. 86 | 87 | class Set 88 | 89 | # Returns the only element of a one-element set. Raises an exception if there 90 | # isn't exactly one element in the set. 91 | 92 | def only 93 | raise IndexError, "Set#only called on non-single-element set" unless size == 1 94 | to_a.first 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /libs/script.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'getoptlong' 3 | require 'facets' 4 | require 'libs/generator' 5 | 6 | module Autumn 7 | 8 | # Manages data used by the `script/generate` and `script/destroy` scripts. 9 | # This class is instantiated by the script, and manages the script's data and 10 | # encapsulates common functionality between the two scripts. The object must 11 | # be initialized and parse_argv must be called before all attributes are ready 12 | # for access. 13 | 14 | class Script 15 | # @return [String] The name of the Autumn object to be created. 16 | attr :name 17 | # @return [String] The type of object to be created (e.g., "leaf"). 18 | attr :object 19 | # @return [:cvs, :svn, :git, nil] The version control system in use for this 20 | # project, or `nil` if none is being used for this transaction. 21 | attr :vcs 22 | # @return [Generator] The Generator instance used to create files. 23 | attr :generator 24 | 25 | # Creates a new instance. 26 | 27 | def initialize 28 | @generator = Autumn::Generator.new 29 | end 30 | 31 | # Parses `ARGV` or similar array. Normally you would pass `ARGV` into this 32 | # method. Populates the `object` and `name` attributes and returns `true`. 33 | # Outputs an error to `STDERR` and returns `false` if the given arguments 34 | # are invalid. 35 | # 36 | # @param [Array] argv The launch arguments. 37 | # @return [true, false] Whether the arguments are valid. 38 | 39 | def parse_argv(argv) 40 | if argv.length != 2 41 | $stderr.puts "Please specify an object (e.g., 'leaf') and its name (e.g., 'Scorekeeper')." 42 | return false 43 | end 44 | 45 | @object = argv.shift 46 | @name = argv.shift 47 | 48 | return true 49 | end 50 | 51 | # Determines the version control system in use by this project and sets the 52 | # {#vcs} attribute to its name (`:cvs`, `:svn`, or `:git`). 53 | 54 | def use_vcs 55 | @vcs = find_vcs 56 | end 57 | 58 | # Calls the method given by the symbol, with two arguments: the `name` 59 | # attribute, and an options hash with verbosity enabled and the VCS set to 60 | # the value of `vcs`. 61 | # 62 | # @param [Symbol] meth The generator method name. 63 | 64 | def call_generator(meth) 65 | generator.send(meth, name, verbose: true, vcs: vcs) 66 | end 67 | 68 | private 69 | 70 | def find_vcs 71 | return :svn if File.exist?('.svn') && File.directory?('.svn') 72 | return :cvs if File.exist?('CVS') && File.directory?('CVS') 73 | return :git if File.exist?('.git') && File.directory?('.git') 74 | return nil 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /libs/speciator.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # The Speciator stores the global, season, stem, and leaf configurations. It 4 | # generates composite hashes, so that any leaf or stem can know its specific 5 | # configuration as a combination of its options and those of the scopes above 6 | # it. 7 | # 8 | # Smaller scopes override larger ones; any season-specific options will 9 | # replace global options, and leaf or stem options will overwrite season 10 | # options. Leaf and stem options are independent from each other, however, 11 | # since leaves and stems share a many-to-many relationship. 12 | # 13 | # Option identifiers can be specified as strings or symbols but are always 14 | # stored as symbols and never accessed as strings. 15 | # 16 | # This is a singleton class; only one instance of it exists for any Autumn 17 | # process. However, for the sake of convenience, many other objects use a 18 | # `config` attribute containing the instance. 19 | 20 | class Speciator 21 | include Singleton 22 | 23 | # Creates a new instance storing no options. 24 | 25 | def initialize 26 | @global_options = Hash.new 27 | @season_options = Hash.new 28 | @stem_options = Hash.autonew 29 | @leaf_options = Hash.autonew 30 | end 31 | 32 | # Returns the global-scope or season-scope config option with the given 33 | # symbol. Season-scope config options will override global ones. 34 | # 35 | # @param [Symbol] sym The option name. 36 | # @return The option value. 37 | 38 | def [](sym) 39 | @season_options[sym] || @global_options[sym] 40 | end 41 | 42 | # @overload global(values) 43 | # Takes a hash of options and values, and sets them at the global scope 44 | # level. 45 | # @param [Hash] values Options to set. 46 | # 47 | # @overload global(option) 48 | # Returns the value for an option at the global scope level. 49 | # @param [Symbol] option An option name. 50 | # @return An option value. 51 | 52 | def global(arg) 53 | arg.kind_of?(Hash) ? @global_options.update(arg.rekey(&:to_sym)) : @global_options[arg] 54 | end 55 | 56 | # @overload season(values) 57 | # Takes a hash of options and values, and sets them at the season scope 58 | # level. Since Autumn can only be run in one season per process, there is 59 | # no need to store the options of specific seasons, only the current 60 | # season. 61 | # @param [Hash] values Options to set. 62 | # 63 | # @overload season(option) 64 | # Returns the value for an option at the season scope level. 65 | # @param [Symbol] option An option name. 66 | # @return An option value. 67 | 68 | def season(arg) 69 | arg.kind_of?(Hash) ? @season_options.update(arg.rekey(&:to_sym)) : @season_options[arg] 70 | end 71 | 72 | # Returns true if the given identifier is a known stem identifier. 73 | # 74 | # @param [String] stem A stem identifier. 75 | # @return [true, false] Whether the stem identifier corresponds to a known 76 | # stem. 77 | 78 | def stem?(stem) 79 | return !@stem_options[stem].nil? 80 | end 81 | 82 | # @overload stem(stem, values) 83 | # Takes a hash of options and values, and sets them at the stem scope 84 | # level. 85 | # @param [Hash] values Options to set. 86 | # 87 | # @overload stem(stem, option) 88 | # Returns the value for an option at the stem scope level. 89 | # @param [Symbol] option An option name. 90 | # @return An option value. 91 | 92 | def stem(stem, arg) 93 | arg.kind_of?(Hash) ? @stem_options[stem].update(arg.rekey(&:to_sym)) : @stem_options[stem][arg] 94 | end 95 | 96 | # Returns true if the given identifier is a known leaf identifier. 97 | # 98 | # @param [String] leaf A leaf identifier. 99 | # @return [true, false] Whether the leaf identifier corresponds to a known 100 | # leaf. 101 | 102 | def leaf?(leaf) 103 | return !@leaf_options[leaf].nil? 104 | end 105 | 106 | # @overload leaf(leaf, values) 107 | # Takes a hash of options and values, and sets them at the leaf scope 108 | # level. 109 | # @param [Hash] values Options to set. 110 | # 111 | # @overload leaf(leaf, option) 112 | # Returns the value for an option at the leaf scope level. 113 | # @param [Symbol] option An option name. 114 | # @return An option value. 115 | 116 | def leaf(leaf, arg) 117 | arg.kind_of?(Hash) ? @leaf_options[leaf].update(arg.rekey(&:to_sym)) : @leaf_options[leaf][arg] 118 | end 119 | 120 | # Visits each stem identifier and its options. 121 | # 122 | # @yield [stem, options] A block to pass to each stem and its options. 123 | # @yieldparam [String] stem The stem identifier. 124 | # @yieldparam [Hash] The stem options. 125 | 126 | def each_stem 127 | @stem_options.each { |stem, options| yield stem, options } 128 | end 129 | 130 | # Visits each leaf identifier and its options. 131 | # 132 | # @yield [leaf, options] A block to pass to each leaf and its options. 133 | # @yieldparam [String] leaf The leaf identifier. 134 | # @yieldparam [Hash] The leaf options. 135 | 136 | def each_leaf 137 | @leaf_options.each { |leaf, options| yield leaf, options } 138 | end 139 | 140 | # @return [Array] An array of all leaf class names in use. 141 | 142 | def all_leaf_classes 143 | @leaf_options.values.collect { |opts| opts[:class] }.uniq 144 | end 145 | 146 | # Returns the composite options for a stem (by identifier), as an 147 | # amalgamation of all the scope levels' options. 148 | # 149 | # @param [String] identifier The stem identifier. 150 | # @return [Hash] The composite stem options. 151 | 152 | def options_for_stem(identifier) 153 | OptionsProxy.new(@global_options, @season_options, @stem_options[identifier]) 154 | end 155 | 156 | # Returns the composite options for a leaf (by identifier), as an 157 | # amalgamation of all the scope levels' options. 158 | # 159 | # @param [String] identifier The leaf identifier. 160 | # @return [Hash] The composite leaf options. 161 | 162 | def options_for_leaf(identifier) 163 | OptionsProxy.new(@global_options, @season_options, @leaf_options[identifier]) 164 | end 165 | end 166 | 167 | # @private 168 | class OptionsProxy 169 | MERGED_METHODS = [:[], :each, :each_key, :each_pair, :each_value, :eql?, 170 | :fetch, :has_key?, :include?, :key?, :member?, :has_value?, :value?, 171 | :hash, :index, :inspect, :invert, :keys, :length, :size, :merge, :reject, 172 | :select, :sort, :to_a, :to_hash, :to_s, :values, :values_at] 173 | 174 | def initialize(*hashes) 175 | raise ArgumentError unless hashes.all? { |hsh| hsh.kind_of? Hash } 176 | @hashes = hashes 177 | @hashes << Hash.new # the runtime settings, which take precedence over all 178 | end 179 | 180 | def method_missing(meth, *args, &block) 181 | if MERGED_METHODS.include?(meth) 182 | merged.send meth, *args, &block 183 | else 184 | returnval = @hashes.last.send(meth, *args, &block) 185 | merged true 186 | returnval 187 | end 188 | end 189 | 190 | private 191 | 192 | def merged(reload=false) 193 | @merged = nil if reload 194 | @merged ||= begin 195 | merged = Hash.new 196 | @hashes.each { |hsh| merged.merge! hsh } 197 | merged 198 | end 199 | end 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /libs/stem_facade.rb: -------------------------------------------------------------------------------- 1 | module Autumn 2 | 3 | # A collection of convenience methods that are added to the Stem class. These 4 | # methods serve two purposes: one, to allow easier backwards compatibility 5 | # with Autumn Leaves 1.0 (which had a simpler one-stem-per-leaf approach), and 6 | # two, to make it easier or more Ruby-like to perform certain IRC actions. 7 | 8 | module StemFacade 9 | 10 | # Sends a message to one or more channels or nicks. If no channels or nicks 11 | # are specified, broadcasts the message to every channel the stem is in. If 12 | # you are sending a message to a channel you must prefix it correctly; the 13 | # "#" will not be added before the channel name for you. 14 | # 15 | # @example 16 | # message "Look at me!" # Broadcasts to all channels 17 | # message "I love kitties", '#kitties' # Sends a message to one channel or person 18 | # message "Learn to RTFM", '#help', 'NoobGuy' # Sends a message to two channels or people 19 | # 20 | # @param [String] msg The message to send. 21 | # @param [Array] chans The channels or nicks to broadcast to 22 | # (channels must include prefix). 23 | 24 | def message(msg, *chans) 25 | return if msg.blank? 26 | chans = channels if chans.empty? 27 | if @throttle 28 | (@message_mutex ||= Mutex.new).synchronize do 29 | msg.each_line { |line| privmsgt chans.to_a, line.strip unless line.strip.empty? } 30 | end 31 | else 32 | msg.each_line { |line| privmsg chans.to_a, line.strip unless line.strip.empty? } 33 | end 34 | end 35 | 36 | # Sets the topic for one or more channels. If no channels are specified, 37 | # sets the topic of every channel the stem is in. 38 | # 39 | # @example 40 | # set_topic "Bots sure are fun!", 'bots', 'morebots' 41 | # 42 | # @param [String] motd The new topic. 43 | # @param [Array] chans The channels to set the topic for. 44 | 45 | def set_topic(motd, *chans) 46 | return if motd.nil? 47 | chans = chans.empty? ? channels : chans.map { |chan| normalized_channel_name chan } 48 | chans.each { |chan| topic chan, motd } 49 | end 50 | 51 | # Joins a channel by name. If the channel is password-protected, specify the 52 | # `password` parameter. Of course, you could always just call the `join` 53 | # method (since each IRC command has a method named after it), but the 54 | # advantage to using this method is that it will also update the 55 | # `@channel_passwords` instance variable. Internal consistency is a good 56 | # thing, so please use this method. 57 | # 58 | # @param [String] channel The channel to join. 59 | # @param [String] password The password for the channel, if it is 60 | # password-protected. 61 | 62 | def join_channel(channel, password=nil) 63 | channel = normalized_channel_name(channel) 64 | return if channels.include? channel 65 | join channel, password 66 | @channel_passwords[channel] = password if password 67 | end 68 | 69 | # Leaves a channel, specified by name. 70 | # 71 | # @param [String] channel The channel to leave. 72 | 73 | def leave_channel(channel) 74 | channel = normalized_channel_name(channel) 75 | return unless channels.include? channel 76 | part channel 77 | end 78 | 79 | # Changes this stem's IRC nick. Note that the stem's original nick will 80 | # still be used by the logger. 81 | # 82 | # @param [String] new_nick The new nickname. 83 | 84 | def change_nick(new_nick) 85 | nick new_nick 86 | end 87 | 88 | # Grants a privilege to a channel member, such as voicing a member. The stem 89 | # must have the required privilege level to perform this command. 90 | # 91 | # @example 92 | # grant_user_privilege 'mychannel', 'Somedude', :operator 93 | # grant_user_privilege '#mychannel', 'Somedude', 'oa' 94 | # 95 | # @param [String] channel The channel to grant the user the privilege for. 96 | # @param [String] nick The user's nickname. 97 | # @param [Symbol, String] privilege The privilege to grant (can either be a 98 | # symbol from the {Daemon} instance or a string with the letter code for 99 | # the privilege.) 100 | 101 | def grant_user_privilege(channel, nick, privilege) 102 | channel = normalized_channel_name(channel) 103 | privcode = server_type.privilege.key(privilege).chr if server_type.privilege.value? privilege 104 | privcode ||= privilege 105 | mode channel, "+#{privcode}", nick 106 | end 107 | 108 | # Removes a privilege to a channel member, such as voicing a member. The 109 | # stem must have the required privilege level to perform this command. 110 | # 111 | # @param [String] channel The channel to revoke the user the privilege for. 112 | # @param [String] nick The user's nickname. 113 | # @param [Symbol, String] privilege The privilege to revoke (can either be a 114 | # symbol from the {Daemon} instance or a string with the letter code for 115 | # the privilege.) 116 | 117 | def remove_user_privilege(channel, nick, privilege) 118 | channel = normalized_channel_name(channel) 119 | privcode = server_type.privilege.key(privilege).chr if server_type.privilege.value? privilege 120 | privcode ||= privilege 121 | mode channel, "-#{privcode}", nick 122 | end 123 | 124 | # Grants a usermode to an IRC nick, such as making a nick invisible. 125 | # The stem must have the required privilege level to perform this command. 126 | # (Generally, one can only change his own usermode unless he is a server 127 | # op.) 128 | # 129 | # @example 130 | # grant_usermode 'Spycloak', :invisible 131 | # grant_usermode 'UpMobility', 'os' 132 | # 133 | # @param [String] nick The user's nickname. 134 | # @param [Symbol, String] property The usermode to set (can either be a 135 | # symbol from the {Daemon} instance or a string with the letter code for 136 | # the usermode.) 137 | 138 | def grant_usermode(nick, property) 139 | propcode = server_type.usermode.key(property).chr if server_type.usermode.value? property 140 | propcode ||= property 141 | mode nick, "+#{propcode}" 142 | end 143 | 144 | # Revokes a usermode from an IRC nick, such as removing invisibility. The 145 | # stem must have the required privilege level to perform this command. 146 | # (Generally, one can only change his own usermode unless he is a server 147 | # op.) 148 | # 149 | # @param [String] nick The user's nickname. 150 | # @param [Symbol, String] property The usermode to remove (can either be a 151 | # symbol from the {Daemon} instance or a string with the letter code for 152 | # the usermode.) 153 | 154 | def remove_usermode(nick, property) 155 | propcode = server_type.usermode.key(property).chr if server_type.usermode.value? property 156 | propcode ||= property 157 | mode nick, "-#{propcode}" 158 | end 159 | 160 | # Sets a property of a channel, such as moderated. The stem must have the 161 | # required privilege level to perform this command. 162 | # 163 | # @example 164 | # set_channel_property '#mychannel', :secret 165 | # set_channel_property 'mychannel', :keylock, 'mypassword' 166 | # set_channel_property '#mychannel', 'ntr' 167 | # 168 | # @param [String] channel The channel name. 169 | # @param [Symbol, String] property The channel mode to remove (can either be 170 | # a symbol from the {Daemon} instance or a string with the letter code for 171 | # the mode.) 172 | # @param [String] argument An argument to provide with the channel mode. 173 | 174 | def set_channel_property(channel, property, argument=nil) 175 | channel = normalized_channel_name(channel) 176 | propcode = server_type.channel_mode.key(property).chr if server_type.channel_mode.value? property 177 | propcode ||= property 178 | mode channel, "+#{propcode}", argument 179 | end 180 | 181 | # Removes a property of a channel, such as moderated. The stem must have the 182 | # required privilege level to perform this command. 183 | # 184 | # @param [String] channel The channel name. 185 | # @param [Symbol, String] property The channel mode to remove (can either be 186 | # a symbol from the {Daemon} instance or a string with the letter code for 187 | # the mode.) 188 | # @param [String] argument An argument to provide with the channel mode. 189 | 190 | def unset_channel_property(channel, property, argument=nil) 191 | channel = normalized_channel_name(channel) 192 | propcode = server_type.channel_mode.key(property).chr if server_type.channel_mode.value? property 193 | propcode ||= property 194 | mode channel, "-#{propcode}", argument 195 | end 196 | 197 | # Returns an array of nicks for users that are in a channel. 198 | # 199 | # @param [String] channel The channel name. 200 | # @return [Array] The users in the channel. 201 | 202 | def users(channel) 203 | channel = normalized_channel_name(channel) 204 | @chan_mutex.synchronize { @channel_members[channel] && @channel_members[channel].keys } 205 | end 206 | 207 | # Returns the privilege level of a channel member. The privilege level will 208 | # be a symbol from the {Daemon} instance. Returns `nil` if the channel 209 | # member doesn't exist or if the bot is not on the given channel. Returns an 210 | # array of privileges if the server supports multiple privileges per user, 211 | # and the user has more than one privilege. 212 | # 213 | # @param [String] channel A channel name. 214 | # @param [String, Hash] user The user nick or sender hash. 215 | # @return [Symbol, Array, nil] The privilege or privileges this user 216 | # has. 217 | 218 | def privilege(channel, user) 219 | user = user[:nick] if user.kind_of? Hash 220 | @chan_mutex.synchronize { @channel_members[channel] && @channel_members[channel][user] } 221 | end 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /resources/daemons/Anothernet.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 320: :rpl_whois_hidden 4 | -------------------------------------------------------------------------------- /resources/daemons/AustHex.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | e: :event 4 | event: 5 | 434: :err_servicenameinuse 6 | 357: :rpl_map 7 | 385: :rpl_notoperanymore 8 | 380: :rpl_yourhelper 9 | 358: :rpl_mapmore 10 | 430: :err_eventnickchange 11 | 320: :rpl_whoisvirt 12 | 309: :rpl_whoishelper 13 | 480: :err_nouline 14 | 359: :rpl_mapend 15 | 310: :rpl_whoisservice 16 | 503: :err_vworldwarn 17 | 520: :err_whotrunc 18 | 377: :rpl_spam 19 | 328: :rpl_channel_url 20 | 240: :rpl_statsxline 21 | 378: :rpl_motd 22 | 307: :rpl_suserhost 23 | usermode: 24 | v: :host_hiding 25 | l: :listall 26 | a: :errors 27 | h: :helper 28 | T: :w_lined 29 | t: :z_lined 30 | -------------------------------------------------------------------------------- /resources/daemons/Bahamut.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | L: :listed 4 | M: :modreg 5 | c: :nocolour 6 | O: :oper_only 7 | R: :regonly 8 | r: :registered 9 | event: 10 | 522: :err_whosyntax 11 | 225: :rpl_statszline 12 | 605: :rpl_nowoff 13 | 484: :err_desync 14 | 440: :err_servicesdown 15 | 429: :err_toomanyaway 16 | 308: :rpl_whoisadmin 17 | 220: :rpl_statsbline 18 | 600: :rpl_logon 19 | 523: :err_wholimexceed 20 | 512: :err_toomanywatch 21 | 468: :err_onlyserverscanchange 22 | 435: :err_banonchan 23 | 226: :rpl_statscount 24 | 617: :rpl_dccstatus 25 | 606: :rpl_watchlist 26 | 408: :err_nocolorsonchan 27 | 309: :rpl_whoissadmin 28 | 265: :rpl_localusers 29 | 601: :rpl_logoff 30 | 227: :rpl_statsgline 31 | 618: :rpl_dcclist 32 | 607: :rpl_endofwatchlist 33 | 310: :rpl_whoissvcmsg 34 | 266: :rpl_globalusers 35 | 222: :rpl_statsbline 36 | 602: :rpl_watchoff 37 | 514: :err_toomanydcc 38 | 338: :rpl_whoisactually 39 | 619: :rpl_endofdcclist 40 | 487: :err_msgservices 41 | 245: :rpl_statssline 42 | 223: :rpl_statseline 43 | 999: :err_numeric_err 44 | 603: :rpl_watchstat 45 | 328: :rpl_channel_url 46 | 620: :rpl_dccinfo 47 | 521: :err_listsyntax 48 | 477: :err_needreggednick 49 | 334: :rpl_commandsyntax 50 | 224: :rpl_statsfline 51 | 604: :rpl_nowon 52 | 329: :rpl_creationtime 53 | 307: :rpl_whoisregnick 54 | usermode: 55 | k: :kills 56 | A: :server_admin 57 | a: :services_admin 58 | m: :spambots 59 | b: :chatops 60 | y: :stats_links 61 | n: :routing 62 | d: :debug 63 | f: :floods 64 | R: :no_non_registered 65 | r: :registered 66 | g: :globops 67 | h: :helper 68 | -------------------------------------------------------------------------------- /resources/daemons/Dancer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | J: :join_throttle 4 | -------------------------------------------------------------------------------- /resources/daemons/GameSurge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 345: :rpl_invited 4 | -------------------------------------------------------------------------------- /resources/daemons/IRCnet.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | R: :reop_list 4 | -------------------------------------------------------------------------------- /resources/daemons/Ithildin.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 771: :rpl_xinfo 4 | 672: :rpl_unknownmodes 5 | 673: :rpl_cannotsetmodes 6 | 773: :rpl_xinfostart 7 | 774: :rpl_xinfoend 8 | -------------------------------------------------------------------------------- /resources/daemons/KineIRCd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | server_mode: 3 | L: :default_language 4 | l: :lazy_leaf 5 | A: :auto_tbs 6 | a: :autoconnect 7 | M: :modes_locked 8 | n: :no_oper 9 | D: :debugging 10 | F: :full 11 | H: :hidden 12 | T: :testlink 13 | channel_mode: 14 | "!": :service 15 | event: 16 | 973: :err_cannotchangeumode 17 | 687: :rpl_yourlanguageis 18 | 665: :rpl_otherumodeis 19 | 979: :err_servermodelock 20 | 682: :rpl_networks 21 | 671: :rpl_whoissecure 22 | 660: :rpl_traceroute_hop 23 | 974: :err_cannotchangechanmode 24 | 688: :rpl_language 25 | 666: :rpl_endof_generic 26 | 980: :err_badcharencoding 27 | 661: :rpl_traceroute_start 28 | 975: :err_cannotchangeservermode 29 | 689: :rpl_whoisstaff 30 | 678: :rpl_luserstaff 31 | 981: :err_toomanylanguages 32 | 662: :rpl_modechangewarn 33 | 976: :err_cannotsendtonick 34 | 690: :rpl_whoislanguage 35 | 679: :rpl_timeonserveris 36 | 982: :err_nolanguage 37 | 663: :rpl_chanredir 38 | 977: :err_unknownservermode 39 | 983: :err_texttooshort 40 | 664: :rpl_servmodeis 41 | 301: :rpl_away 42 | 4: :rpl_myinfo 43 | 670: :rpl_whowasdetails 44 | user_prefix: 45 | "!": :service 46 | usermode: 47 | d: :deaf 48 | R: :no_non_registered 49 | g: :callerid 50 | s: :server_notices 51 | h: :helper 52 | channel_prefix: 53 | .: :softchan 54 | "~": :global 55 | privilege: 56 | "!": :service 57 | -------------------------------------------------------------------------------- /resources/daemons/PTlink.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 247: :rpl_statsxline 4 | 484: :err_desync 5 | 485: :err_cantkickadmin 6 | 615: :rpl_mapmore 7 | -------------------------------------------------------------------------------- /resources/daemons/QuakeNet.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | D: :deljoins 4 | d: :has_hidden 5 | u: :noquitparts 6 | event: 7 | 291: :rpl_endofcheck 8 | 550: :err_badhostmask 9 | 286: :rpl_chkhead 10 | 551: :err_hostunavail 11 | 485: :err_isrealservice 12 | 287: :rpl_chanuser 13 | 552: :err_usingsline 14 | 486: :err_accountonly 15 | 288: :rpl_patchhead 16 | 553: :err_statssline 17 | 355: :rpl_namreply_ 18 | 289: :rpl_patchcon 19 | 290: :rpl_datastr 20 | 285: :rpl_newhostis 21 | -------------------------------------------------------------------------------- /resources/daemons/RFC1459.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | v: :voice 4 | k: :keylock 5 | l: :limit 6 | m: :moderated 7 | b: :ban 8 | n: :no_external_msgs 9 | o: :oper 10 | p: :private 11 | s: :secret 12 | t: :topic_lock 13 | i: :invite_only 14 | event: 15 | 462: :err_alreadyregistered 16 | 203: :rpl_traceunknown 17 | 314: :rpl_whowasuser 18 | 351: :rpl_version 19 | 481: :err_noprivileges 20 | 407: :err_toomanytargets 21 | 444: :err_nologin 22 | 259: :rpl_adminemail 23 | 463: :err_nopermforhost 24 | 204: :rpl_traceoperator 25 | 241: :rpl_statslline 26 | 315: :rpl_endofwho 27 | 352: :rpl_whoreply 28 | 482: :err_chanoprivsneeded 29 | 445: :err_summondisabled 30 | 371: :rpl_info 31 | 501: :err_umodeunknownflag 32 | 464: :err_passwdmismatch 33 | 205: :rpl_traceuser 34 | 242: :rpl_statsuptime 35 | 316: :rpl_whoischanop 36 | 353: :rpl_namreply 37 | 483: :err_cantkillserver 38 | 261: :rpl_tracelog 39 | 372: :rpl_motd 40 | 409: :err_noorigin 41 | 446: :err_usersdisabled 42 | 502: :err_usersdontmatch 43 | 465: :err_yourebannedcreep 44 | 206: :rpl_traceserver 45 | 243: :rpl_statsoline 46 | 317: :rpl_whoisidle 47 | 391: :rpl_time 48 | 373: :rpl_infostart 49 | 466: :err_youwillbebanned 50 | 244: :rpl_statshline 51 | 318: :rpl_endofwhois 52 | 392: :rpl_usersstart 53 | 300: :rpl_none 54 | 374: :rpl_endofinfo 55 | 411: :err_norecipient 56 | 467: :err_keyset 57 | 208: :rpl_tracenewtype 58 | 319: :rpl_whoischannels 59 | 393: :rpl_users 60 | 301: :rpl_away 61 | 375: :rpl_motdstart 62 | 412: :err_notexttosend 63 | 394: :rpl_endofusers 64 | 431: :err_nonicknamegiven 65 | 302: :rpl_userhost 66 | 376: :rpl_endofmotd 67 | 413: :err_notoplevel 68 | 321: :rpl_liststart 69 | 395: :rpl_nousers 70 | 432: :err_erroneusnickname 71 | 303: :rpl_ison 72 | 414: :err_wildtoplevel 73 | 451: :err_notregistered 74 | 322: :rpl_list 75 | 433: :err_nicknameinuse 76 | 211: :rpl_statslinkinfo 77 | 341: :rpl_inviting 78 | 471: :err_channelisfull 79 | 212: :rpl_statscommands 80 | 323: :rpl_listend 81 | 231: :rpl_serviceinfo 82 | 305: :rpl_unaway 83 | 342: :rpl_summoning 84 | 472: :err_unknownmode 85 | 213: :rpl_statscline 86 | 324: :rpl_channelmodeis 87 | 361: :rpl_killdone 88 | 491: :err_nooperhost 89 | 232: :rpl_endofservices 90 | 306: :rpl_nowaway 91 | 473: :err_inviteonlychan 92 | 436: :err_nickcollision 93 | 214: :rpl_statsnline 94 | 251: :rpl_luserclient 95 | 362: :rpl_closing 96 | 492: :err_noservicehost 97 | 233: :rpl_service 98 | 381: :rpl_youreoper 99 | 474: :err_bannedfromchan 100 | 215: :rpl_statsiline 101 | 252: :rpl_luserop 102 | 363: :rpl_closeend 103 | 382: :rpl_rehashing 104 | 475: :err_badchannelkey 105 | 216: :rpl_statskline 106 | 253: :rpl_luserunknown 107 | 364: :rpl_links 108 | 401: :err_nosuchnick 109 | 217: :rpl_statsqline 110 | 254: :rpl_luserchannels 111 | 365: :rpl_endoflinks 112 | 402: :err_nosuchserver 113 | 384: :rpl_myportis 114 | 421: :err_unknowncommand 115 | 255: :rpl_luserme 116 | 366: :rpl_endofnames 117 | 403: :err_nosuchchannel 118 | 218: :rpl_statsyline 119 | 311: :rpl_whoisuser 120 | 422: :err_nomotd 121 | 200: :rpl_tracelink 122 | 367: :rpl_banlist 123 | 404: :err_cannotsendtochan 124 | 441: :err_usernotinchannel 125 | 219: :rpl_endofstats 126 | 256: :rpl_adminme 127 | 312: :rpl_whoisserver 128 | 423: :err_noadmininfo 129 | 201: :rpl_traceconnecting 130 | 331: :rpl_notopic 131 | 368: :rpl_endofbanlist 132 | 405: :err_toomanychannels 133 | 442: :err_notonchannel 134 | 257: :rpl_adminloc1 135 | 424: :err_fileerror 136 | 461: :err_needmoreparams 137 | 202: :rpl_tracehandshake 138 | 313: :rpl_whoisoperator 139 | 369: :rpl_endofwhowas 140 | 406: :err_wasnosuchnick 141 | 443: :err_useronchannel 142 | 221: :rpl_umodeis 143 | 258: :rpl_adminloc2 144 | 332: :rpl_topic 145 | user_prefix: 146 | "@": :operator 147 | +: :voiced 148 | usermode: 149 | w: :wallops 150 | o: :global_operator 151 | s: :server_notices 152 | i: :invisible 153 | channel_prefix: 154 | "#": :network 155 | "&": :local 156 | privilege: 157 | v: :voiced 158 | o: :operator 159 | -------------------------------------------------------------------------------- /resources/daemons/RFC2811.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | a: :anonymous 4 | O: :creator 5 | e: :ban_exception 6 | q: :quiet 7 | r: :reop 8 | h: :halfop 9 | I: :invitation_mask 10 | channel_prefix: 11 | +: :network_unmoderated 12 | "!": :network_safe 13 | privilege: 14 | O: :creator 15 | usermode: 16 | "@": :creator 17 | -------------------------------------------------------------------------------- /resources/daemons/RFC2812.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 478: :err_banlistfull 4 | 346: :rpl_invitelist 5 | 247: :rpl_statsbline 6 | 5: :rpl_bounce 7 | 484: :err_restricted 8 | 209: :rpl_traceclass 9 | 347: :rpl_endofinvitelist 10 | 325: :rpl_uniqopis 11 | 485: :err_uniqoprivsneeded 12 | 408: :err_nosuchservice 13 | 210: :rpl_tracereconnect 14 | 1: :rpl_welcome 15 | 348: :rpl_exceptlist 16 | 2: :rpl_yourhost 17 | 437: :err_unavailresource 18 | 415: :err_badmask 19 | 349: :rpl_endofexceptlist 20 | 250: :rpl_statsdline 21 | 476: :err_badchanmask 22 | 234: :rpl_servlist 23 | 3: :rpl_created 24 | 383: :rpl_youreservice 25 | 262: :rpl_traceend 26 | 240: :rpl_statsvline 27 | 207: :rpl_traceservice 28 | 477: :err_nochanmodes 29 | 246: :rpl_statsping 30 | 235: :rpl_servlistend 31 | 4: :rpl_myinfo 32 | 263: :rpl_tryagain 33 | usermode: 34 | a: :away 35 | O: :local_operator 36 | r: :restricted 37 | -------------------------------------------------------------------------------- /resources/daemons/RatBox.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 720: :rpl_omotdstart 4 | 709: :rpl_etrace 5 | 726: :rpl_notestline 6 | 715: :err_knockdisabled 7 | 704: :rpl_helpstart 8 | 721: :rpl_omotd 9 | 710: :rpl_knock 10 | 716: :rpl_targumodeg 11 | 705: :rpl_helptxt 12 | 722: :rpl_endofomotd 13 | 711: :rpl_knockdlvr 14 | 717: :rpl_targnotify 15 | 706: :rpl_endofhelp 16 | 723: :err_noprivs 17 | 712: :err_toomanyknock 18 | 718: :rpl_umodegmsg 19 | 724: :rpl_testmark 20 | 713: :err_chanopen 21 | 702: :rpl_modlist 22 | 708: :rpl_etracefull 23 | 725: :rpl_testline 24 | 714: :err_knockonchan 25 | 703: :rpl_endofmodlist 26 | -------------------------------------------------------------------------------- /resources/daemons/Ultimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 621: :rpl_rules 4 | 610: :rpl_isoper 5 | 434: :err_norules 6 | 616: :rpl_whoishost 7 | 275: :rpl_statsdline 8 | 622: :rpl_endofrules 9 | 611: :rpl_islocop 10 | 617: :rpl_whoisbot 11 | 386: :rpl_ircops 12 | 623: :rpl_mapmore 13 | 612: :rpl_isnotoper 14 | 387: :rpl_endofircops 15 | 624: :rpl_omotdstart 16 | 613: :rpl_endofisoper 17 | 630: :rpl_settings 18 | 619: :rpl_whowashost 19 | 608: :rpl_watchclear 20 | 625: :rpl_omotd 21 | 631: :rpl_endofsettings 22 | 620: :rpl_rulesstart 23 | 626: :rpl_endofo 24 | 615: :rpl_whoismodes 25 | -------------------------------------------------------------------------------- /resources/daemons/Undernet.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 489: :err_voiceneeded 4 | 484: :err_ischanservice 5 | 396: :rpl_hosthidden 6 | 449: :err_notimplemented 7 | -------------------------------------------------------------------------------- /resources/daemons/Unreal.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | V: :no_invite 4 | K: :noknock 5 | L: :link 6 | A: :admin_only 7 | a: :channel_protection 8 | M: :regonly 9 | N: :no_nick_change 10 | C: :no_ctcp 11 | z: :secured_only 12 | O: :oper_only 13 | Q: :no_kick 14 | q: :channel_owner 15 | f: :flood_limit 16 | G: :strip_bad_words 17 | S: :strip 18 | H: :no_hiding 19 | u: :auditorium 20 | event: 21 | 389: :rpl_endofalist 22 | 294: :rpl_helpfwd 23 | 484: :err_attackdeny 24 | 247: :rpl_statsxline 25 | 295: :rpl_helpign 26 | 447: :err_nonickchange 27 | 485: :err_killdeny 28 | 334: :rpl_listsyntax 29 | 486: :err_htmdisabled 30 | 524: :err_operspverify 31 | 600: :rpl_logon 32 | 220: :rpl_statsbline 33 | 477: :err_needreggednick 34 | 610: :rpl_mapmore 35 | 601: :rpl_logoff 36 | 468: :err_onlyserverscanchange 37 | 335: :rpl_whoisbot 38 | 972: :err_cannotdocommand 39 | 307: :rpl_whoisregnick 40 | 250: :rpl_statsconn 41 | 459: :err_nohiding 42 | 440: :err_servicesdown 43 | 602: :rpl_watchoff 44 | 222: :rpl_sqline_nick 45 | 640: :rpl_dumping 46 | 469: :err_linkset 47 | 460: :err_notforhalfops 48 | 232: :rpl_rules 49 | 479: :err_linkfail 50 | 308: :rpl_rulesstart 51 | 603: :rpl_watchstat 52 | 223: :rpl_statsgline 53 | 641: :rpl_dumprpl 54 | 470: :err_linkchannel 55 | 489: :err_secureonlychan 56 | 290: :rpl_helphdr 57 | 499: :err_chanownprivneeded 58 | 480: :err_cannotknock 59 | 309: :rpl_endofrules 60 | 518: :err_noinvite 61 | 385: :rpl_notoperanymore 62 | 642: :rpl_eodump 63 | 604: :rpl_nowon 64 | 224: :rpl_statstline 65 | 310: :rpl_whoishelpop 66 | 519: :err_admonly 67 | 386: :rpl_qlist 68 | 6: :rpl_map 69 | 291: :rpl_helpop 70 | 320: :rpl_whoisspecial 71 | 605: :rpl_nowoff 72 | 225: :rpl_statseline 73 | 434: :err_norules 74 | 520: :err_operonly 75 | 387: :rpl_endofqlist 76 | 7: :rpl_mapend 77 | 425: :err_noopermotd 78 | 292: :rpl_helptlr 79 | 378: :rpl_whoishost 80 | 606: :rpl_watchlist 81 | 226: :rpl_statsnline 82 | 435: :err_serviceconfused 83 | 388: :rpl_alist 84 | 293: :rpl_helphlp 85 | 455: :err_hostilename 86 | 607: :rpl_endofwatchlist 87 | 227: :rpl_statsvline 88 | 379: :rpl_whoismodes 89 | usermode: 90 | V: :webtv 91 | v: :victim 92 | W: :whois_paranoia 93 | A: :server_admin 94 | a: :services_admin 95 | x: :host_hiding 96 | B: :bot 97 | b: :chatops 98 | N: :network_admin 99 | C: :co_admin 100 | z: :secure_conn 101 | p: :hide_channels 102 | q: :kix 103 | R: :no_non_registered 104 | G: :strip_bad_words 105 | S: :service 106 | s: :server_notices 107 | H: :hide_oper 108 | T: :block_ctcp 109 | t: :modified_host 110 | I: :invisible_joinpart 111 | -------------------------------------------------------------------------------- /resources/daemons/_Other.yml: -------------------------------------------------------------------------------- 1 | --- 2 | privilege: 3 | a: :admin # Compatibility with irc.utonet.org 4 | user_prefix: 5 | "~": :channel_owner # Compatibility with irc.utonet.org 6 | "&": :admin # Compatibility with irc.utonet.org 7 | 8 | -------------------------------------------------------------------------------- /resources/daemons/aircd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 379: :rpl_kicklinked 4 | 291: :rpl_chaninfo_banned 5 | 269: :rpl_end_netstat 6 | 308: :rpl_notifyaction 7 | 286: :rpl_chaninfo_users 8 | 380: :rpl_banlinked 9 | 292: :rpl_chaninfo_bans 10 | 50: :rpl_attemptingjunc 11 | 419: :err_lengthtruncated 12 | 309: :rpl_nicktrace 13 | 287: :rpl_chaninfo_chops 14 | 265: :rpl_localusers 15 | 210: :rpl_stats 16 | 293: :rpl_chaninfo_invite 17 | 51: :rpl_attemptingreroute 18 | 299: :rpl_end_chaninfo 19 | 288: :rpl_chaninfo_voices 20 | 266: :rpl_globalusers 21 | 470: :err_kickedfromchan 22 | 294: :rpl_chaninfo_invites 23 | 377: :rpl_kickexpired 24 | 289: :rpl_chaninfo_away 25 | 267: :rpl_start_netstat 26 | 295: :rpl_chaninfo_kick 27 | 273: :rpl_notify 28 | 378: :rpl_banexpired 29 | 290: :rpl_chaninfo_opers 30 | 268: :rpl_netstat 31 | 296: :rpl_chaninfo_kicks 32 | 285: :rpl_chaninfo_handle 33 | 274: :rpl_endnotify 34 | -------------------------------------------------------------------------------- /resources/daemons/bdq-ircd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 391: :rpl_time 4 | -------------------------------------------------------------------------------- /resources/daemons/hybrid.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | a: :hideops 4 | p: :private 5 | event: 6 | 247: :rpl_statsxline 7 | 225: :rpl_statsdline 8 | 484: :err_desync 9 | 385: :rpl_notoperanymore 10 | 220: :rpl_statspline 11 | 479: :err_badchanname 12 | 265: :rpl_localusers 13 | 249: :rpl_statsdebug 14 | 266: :rpl_globalusers 15 | 503: :err_ghostedclient 16 | 245: :rpl_statssline 17 | 14: :rpl_yourcookie 18 | 246: :rpl_statsuline 19 | 224: :rpl_statsfline 20 | 477: :err_needreggednick 21 | 42: :rpl_youruniqueid 22 | user_prefix: 23 | "%": :half_operator 24 | usermode: 25 | k: :kills 26 | l: :locops 27 | a: :admin 28 | x: :external 29 | b: :bots 30 | y: :spy 31 | n: :nchange 32 | c: :client_conns 33 | z: :operwall 34 | d: :debug 35 | f: :full 36 | r: :rej 37 | g: :callerid 38 | u: :unauth 39 | privilege: 40 | h: :half_operator 41 | -------------------------------------------------------------------------------- /resources/daemons/hyperion.yml: -------------------------------------------------------------------------------- 1 | --- 2 | event: 3 | 477: :err_needreggednick 4 | 250: :rpl_statsconn 5 | -------------------------------------------------------------------------------- /resources/daemons/ircu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | server_mode: 3 | s: :service 4 | h: :hub 5 | channel_mode: 6 | r: :regonly 7 | event: 8 | 511: :err_silelistfull 9 | 280: :rpl_glist 10 | 247: :rpl_statsgline 11 | 236: :rpl_statsverbose 12 | 16: :rpl_mapmore 13 | 517: :err_disabled 14 | 495: :err_badlogtype 15 | 330: :rpl_whoisaccount 16 | 275: :rpl_statsdline 17 | 468: :err_invalidusername 18 | 391: :rpl_time 19 | 281: :rpl_endofglist 20 | 270: :rpl_privs 21 | 248: :rpl_statsuline 22 | 237: :rpl_statsengine 23 | 17: :rpl_mapend 24 | 518: :err_longmask 25 | 496: :err_badlogsys 26 | 524: :err_quarantined 27 | 513: :err_badping 28 | 282: :rpl_jupelist 29 | 271: :rpl_silelist 30 | 238: :rpl_statsfline 31 | 519: :err_toomanyusers 32 | 497: :err_badlogvalue 33 | 354: :rpl_whospcrpl 34 | 514: :err_invalid_error 35 | 437: :err_bannickchange 36 | 338: :rpl_whoisactually 37 | 283: :rpl_endofjupelist 38 | 272: :rpl_endofsilelist 39 | 250: :rpl_statsconn 40 | 228: :rpl_statsqline 41 | 217: :rpl_statspline 42 | 8: :rpl_snomask 43 | 520: :err_masktoowide 44 | 498: :err_isoperlchan 45 | 333: :rpl_topicwhotime 46 | 515: :err_badexpire 47 | 493: :err_nofeature 48 | 438: :err_nicktoofast 49 | 416: :err_querytoolong 50 | 284: :rpl_feature 51 | 9: :rpl_statmemtot 52 | 477: :err_needreggednick 53 | 334: :rpl_listusage 54 | 246: :rpl_statstline 55 | 15: :rpl_map 56 | 516: :err_dontcheat 57 | 494: :err_badfeature 58 | 439: :err_targettoofast 59 | 340: :rpl_userip 60 | 10: :rpl_statmem 61 | usermode: 62 | k: :service 63 | x: :host_hiding 64 | d: :deaf 65 | r: :registered 66 | g: :debug 67 | s: :server_notices 68 | -------------------------------------------------------------------------------- /resources/daemons/tr-ircd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | channel_mode: 3 | R: :regonly 4 | u: :founder 5 | user_prefix: 6 | .: :founder 7 | privilege: 8 | u: :founder 9 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Launches an IRb console with the Autumn environment loaded. 4 | 5 | $: << Dir.getwd 6 | require 'libs/autumn' 7 | 8 | # Some code below is from Ruby on Rails and copyright (c) David Heinemeier 9 | # Hansson. 10 | 11 | irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' 12 | 13 | require 'optparse' 14 | options = { irb: irb } 15 | OptionParser.new do |opt| 16 | opt.banner = "Usage: #{File.basename(__FILE__)} [options]" 17 | opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |v| options[:irb] = v } 18 | opt.parse!(ARGV) 19 | end 20 | 21 | libs = ' -r irb/completion' 22 | libs << ' -r libs/console_boot' 23 | 24 | require 'libs/genesis' 25 | puts "Loading Autumn #{Autumn::Config.version}..." 26 | exec "#{options[:irb]} -I. #{libs}" 27 | -------------------------------------------------------------------------------- /script/daemon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Controller for the Autumn daemon. Starts, stops, and manages the daemon. Must 4 | # be run from the Autumn root directory. 5 | # 6 | # Usage: `script/daemon -- ` 7 | # 8 | # where is one of: 9 | # 10 | # | | | 11 | # |:----------|:-----------------------------------------------| 12 | # | `start` | start an instance of the application | 13 | # | `stop` | stop all instances of the application | 14 | # | `restart` | stop all instances and restart them afterwards | 15 | # | `run` | start the application and stay on top | 16 | # | `zap` | set the application to a stopped state | 17 | # 18 | # and where may contain several of the following 19 | # 20 | # | | | 21 | # |:----------------|:------------------------------------------------------------------------------------------------------| 22 | # | `-t`, `--ontop` | Stay on top (does not daemonize) | 23 | # | `-f`, `--force` | Force operation | 24 | # | `--help`, `-h` | Displays this usage information. | 25 | # | `--vcs`, `-c` | Add any created files or directories to the project's version control system (normally auto-detects). | 26 | 27 | require 'rubygems' 28 | require 'daemons' 29 | 30 | $: << Dir.getwd 31 | require 'libs/autumn' 32 | 33 | Daemons.run "#{Autumn::Config.root}/script/server", 34 | app_name: 'autumn', 35 | dir_mode: :normal, 36 | dir: "#{Autumn::Config.root}/tmp", 37 | multiple: false, 38 | backtrace: true, 39 | monitor: false, 40 | log_output: true 41 | -------------------------------------------------------------------------------- /script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Synopsis 4 | # -------- 5 | # 6 | # Destroys the files for leaves, seasons, and other objects of the Autumn 7 | # framework. 8 | # 9 | # Usage 10 | # ----- 11 | # 12 | # ```` sh 13 | # script/destroy 14 | # ```` 15 | # 16 | # | | | 17 | # |:---------|:-----------------------------------------------------------------------------------------------------------------------------------| 18 | # | `object` | The object type to destroy. Valid types are "leaf" and "season". | 19 | # | `name` | The name of the object to destroy. For example, you can call `script/destroy leaf Scorekeeper` to remove a leaf named Scorekeeper. | 20 | # 21 | # Options 22 | # ------- 23 | # 24 | # | | | 25 | # |:---------------|:------------------------------------------------------------------------------------------------------| 26 | # | `--help`, `-h` | Displays this usage information. | 27 | # | `--vcs`, `-c` | Add any created files or directories to the project's version control system (normally auto-detects). | 28 | 29 | $: << Dir.getwd 30 | require 'libs/script' 31 | 32 | opts = GetoptLong.new( 33 | ['--help', '-h', GetoptLong::NO_ARGUMENT], 34 | ['--vcs', '-c', GetoptLong::NO_ARGUMENT] 35 | ) 36 | 37 | script = Autumn::Script.new 38 | 39 | begin 40 | opts.each do |opt, _| 41 | case opt 42 | when '--help' then RDoc::usage 43 | when '--vcs' then script.use_vcs 44 | end 45 | end 46 | rescue GetoptLong::InvalidOption 47 | RDoc::usage 48 | exit 0 49 | end 50 | 51 | exit(0) unless script.parse_argv(ARGV) 52 | 53 | case script.object 54 | when 'leaf' then script.call_generator(:unleaf) 55 | when 'season' then script.call_generator(:unseason) 56 | end 57 | -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Synopsis 4 | # -------- 5 | # 6 | # Generates template files for leaves, seasons, and other Autumn objects. 7 | # 8 | # Usage 9 | # ----- 10 | # 11 | # 12 | # ```` sh 13 | # script/generate