lita-karma 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +22 -6
- data/lib/lita-karma.rb +11 -1
- data/lib/lita/handlers/karma/chat.rb +206 -0
- data/lib/lita/handlers/karma/config.rb +20 -0
- data/lib/lita/handlers/karma/term.rb +152 -0
- data/lita-karma.gemspec +1 -1
- data/locales/en.yml +40 -0
- data/spec/lita/handlers/{karma_spec.rb → karma/chat_spec.rb} +28 -41
- metadata +10 -6
- data/lib/lita/handlers/karma.rb +0 -315
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: e1e03d87e444b488534df84e8c3d16a8f02f6c82
         | 
| 4 | 
            +
              data.tar.gz: 45ad68d97c0bb3b3e172a1334c8d4ad5e29af53f
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 457f520bc9e527737878628b873e85dc86f2449b45d77c3ac3506a8e81276017311e4db61a312d8e01981652ab733d316be3cb936e8da8a14a115ee5e7f663b0
         | 
| 7 | 
            +
              data.tar.gz: 8625c91141b4f995f4be50fe63c72e06ff89643f7f423330d79b43a00c367454059defc608841b795fd31e775973a6375965effbf9c67050dc6e628488a466df
         | 
    
        data/README.md
    CHANGED
    
    | @@ -18,11 +18,10 @@ gem "lita-karma" | |
| 18 18 |  | 
| 19 19 | 
             
            ### Optional attributes
         | 
| 20 20 |  | 
| 21 | 
            -
            *  | 
| 22 | 
            -
            *  | 
| 23 | 
            -
            *  | 
| 24 | 
            -
            *  | 
| 25 | 
            -
             | 
| 21 | 
            +
            * **cooldown** (`Integer`, `nil`) - Controls how long a user must wait after modifying a term before they can modify it again. The value should be an integer number of seconds. Set it to `nil` to disable rate limiting. Default: `300` (5 minutes).
         | 
| 22 | 
            +
            * **link_karma_threshold** (`Integer`, `nil`) - Controls how many points a term must have before it can be linked to other terms or before terms can be linked to it. Treated as an absolute value, so it applies to both positive and negative karma. Set it to `nil` to allow all terms to be linked regardless of karma. Default: `10`.
         | 
| 23 | 
            +
            * **term_pattern** (`Regexp`) - Determines what Lita will recognize as a valid term for tracking karma. Default: `/[\[\]\p{Word}\._|\{\}]{2,}/`.
         | 
| 24 | 
            +
            * **term_normalizer** (`#call`) - A custom callable that determines how each term will be normalized before being stored in Redis. The proc should take one argument, the term as matched via regular expression, and return one value, the normalized version of the term. Default: turns the term into a string, downcases it, and strips whitespace off both ends.
         | 
| 26 25 |  | 
| 27 26 | 
             
            ### Example
         | 
| 28 27 |  | 
| @@ -40,6 +39,8 @@ end | |
| 40 39 |  | 
| 41 40 | 
             
            ## Usage
         | 
| 42 41 |  | 
| 42 | 
            +
            ### Giving Karma
         | 
| 43 | 
            +
             | 
| 43 44 | 
             
            Lita will add a karma point whenever it hears a term upvoted:
         | 
| 44 45 |  | 
| 45 46 | 
             
            ```
         | 
| @@ -58,6 +59,8 @@ To check the current karma for a term without modifying it: | |
| 58 59 | 
             
            term~~
         | 
| 59 60 | 
             
            ```
         | 
| 60 61 |  | 
| 62 | 
            +
            ### Listing Karma
         | 
| 63 | 
            +
             | 
| 61 64 | 
             
            To list the top scoring terms:
         | 
| 62 65 |  | 
| 63 66 | 
             
            ```
         | 
| @@ -76,12 +79,14 @@ To list the worst scoring terms: | |
| 76 79 | 
             
            Lita: karma worst
         | 
| 77 80 | 
             
            ```
         | 
| 78 81 |  | 
| 79 | 
            -
             | 
| 82 | 
            +
            The list commands will list 5 terms by default. To specify a number (no greater than 25), pass a second argument to the karma command:
         | 
| 80 83 |  | 
| 81 84 | 
             
            ```
         | 
| 82 85 | 
             
            Lita: karma best 10
         | 
| 83 86 | 
             
            ```
         | 
| 84 87 |  | 
| 88 | 
            +
            ### Linking Terms
         | 
| 89 | 
            +
             | 
| 85 90 | 
             
            You can also link terms together. This adds one term's karma to another's whenever it is displayed. A link is uni-directional and non-destructive. You can unlink terms at any time.
         | 
| 86 91 |  | 
| 87 92 | 
             
            ```
         | 
| @@ -103,6 +108,17 @@ foo~~ | |
| 103 108 |  | 
| 104 109 | 
             
            When a term is linked, the total karma score is displayed first, followed by the score of the term without its linked terms in parentheses.
         | 
| 105 110 |  | 
| 111 | 
            +
            ### Modification lists
         | 
| 112 | 
            +
             | 
| 113 | 
            +
            To get a list of the users who have upvoted or downvoted a term:
         | 
| 114 | 
            +
             | 
| 115 | 
            +
            ```
         | 
| 116 | 
            +
            Lita: karma modified foo
         | 
| 117 | 
            +
            > Joe, Amy
         | 
| 118 | 
            +
            ```
         | 
| 119 | 
            +
             | 
| 120 | 
            +
            ### Deleting Terms
         | 
| 121 | 
            +
             | 
| 106 122 | 
             
            To permanently delete a term and all its links:
         | 
| 107 123 |  | 
| 108 124 | 
             
            ```
         | 
    
        data/lib/lita-karma.rb
    CHANGED
    
    | @@ -1 +1,11 @@ | |
| 1 | 
            -
            require " | 
| 1 | 
            +
            require "set"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "lita"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Lita.load_locales Dir[File.expand_path(
         | 
| 6 | 
            +
              File.join("..", "..", "locales", "*.yml"), __FILE__
         | 
| 7 | 
            +
            )]
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require "lita/handlers/karma/chat"
         | 
| 10 | 
            +
            require 'lita/handlers/karma/config'
         | 
| 11 | 
            +
            require 'lita/handlers/karma/term'
         | 
| @@ -0,0 +1,206 @@ | |
| 1 | 
            +
            module Lita::Handlers::Karma
         | 
| 2 | 
            +
              # Tracks karma points for arbitrary terms.
         | 
| 3 | 
            +
              class Chat < Lita::Handler
         | 
| 4 | 
            +
                namespace "karma"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                on :loaded, :define_routes
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def define_routes(payload)
         | 
| 9 | 
            +
                  define_static_routes
         | 
| 10 | 
            +
                  define_dynamic_routes(config.term_pattern.source)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def increment(response)
         | 
| 14 | 
            +
                  modify(response, :increment)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def decrement(response)
         | 
| 18 | 
            +
                  modify(response, :decrement)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def check(response)
         | 
| 22 | 
            +
                  seen = Set.new
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  output = response.matches.map do |match|
         | 
| 25 | 
            +
                    term = get_term(match[0])
         | 
| 26 | 
            +
                    next if seen.include?(term)
         | 
| 27 | 
            +
                    seen << term
         | 
| 28 | 
            +
                    term.check
         | 
| 29 | 
            +
                  end.compact
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  response.reply output.join("; ")
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def list_best(response)
         | 
| 35 | 
            +
                  list(response, :list_best)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def list_worst(response)
         | 
| 39 | 
            +
                  list(response, :list_worst)
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def link(response)
         | 
| 43 | 
            +
                  response.matches.each do |match|
         | 
| 44 | 
            +
                    term1 = get_term(match[0])
         | 
| 45 | 
            +
                    term2 = get_term(match[1])
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    result = term1.link(term2)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    case result
         | 
| 50 | 
            +
                    when Integer
         | 
| 51 | 
            +
                      response.reply t("threshold_not_satisfied", threshold: result)
         | 
| 52 | 
            +
                    when true
         | 
| 53 | 
            +
                      response.reply t("link_success", source: term2, target: term1)
         | 
| 54 | 
            +
                    else
         | 
| 55 | 
            +
                      response.reply t("already_linked", source: term2, target: term1)
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def unlink(response)
         | 
| 61 | 
            +
                  response.matches.each do |match|
         | 
| 62 | 
            +
                    term1 = get_term(match[0])
         | 
| 63 | 
            +
                    term2 = get_term(match[1])
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    if term1.unlink(term2)
         | 
| 66 | 
            +
                      response.reply t("unlink_success", source: term2, target: term1)
         | 
| 67 | 
            +
                    else
         | 
| 68 | 
            +
                      response.reply t("already_unlinked", source: term2, target: term1)
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def modified(response)
         | 
| 74 | 
            +
                  term = get_term(response.args[1])
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  users = term.modified
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  if users.empty?
         | 
| 79 | 
            +
                    response.reply t("never_modified", term: term)
         | 
| 80 | 
            +
                  else
         | 
| 81 | 
            +
                    response.reply users.map(&:name).join(", ")
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def delete(response)
         | 
| 86 | 
            +
                  term = Term.new(robot, response.message.body.sub(/^karma delete /, ""), normalize: false)
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  if term.delete
         | 
| 89 | 
            +
                    response.reply t("delete_success", term: term)
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                private
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def define_dynamic_routes(pattern)
         | 
| 96 | 
            +
                  self.class.route(
         | 
| 97 | 
            +
                    %r{(#{pattern})\+\+#{token_terminator.source}},
         | 
| 98 | 
            +
                    :increment,
         | 
| 99 | 
            +
                    help: { t("help.increment_key") => t("help.increment_value") }
         | 
| 100 | 
            +
                  )
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  self.class.route(
         | 
| 103 | 
            +
                    %r{(#{pattern})--#{token_terminator.source}},
         | 
| 104 | 
            +
                    :decrement,
         | 
| 105 | 
            +
                    help: { t("help.decrement_key") => t("help.decrement_value") }
         | 
| 106 | 
            +
                  )
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  self.class.route(
         | 
| 109 | 
            +
                    %r{(#{pattern})~~#{token_terminator.source}},
         | 
| 110 | 
            +
                    :check,
         | 
| 111 | 
            +
                    help: { t("help.check_key") => t("help.check_value") }
         | 
| 112 | 
            +
                  )
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  self.class.route(
         | 
| 115 | 
            +
                    %r{^(#{pattern})\s*\+=\s*(#{pattern})(?:\+\+|--|~~)?#{token_terminator.source}},
         | 
| 116 | 
            +
                    :link,
         | 
| 117 | 
            +
                    command: true,
         | 
| 118 | 
            +
                    help: { t("help.link_key") => t("help.link_value") }
         | 
| 119 | 
            +
                  )
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  self.class.route(
         | 
| 122 | 
            +
                    %r{^(#{pattern})\s*-=\s*(#{pattern})(?:\+\+|--|~~)?#{token_terminator.source}},
         | 
| 123 | 
            +
                    :unlink,
         | 
| 124 | 
            +
                    command: true,
         | 
| 125 | 
            +
                    help: { t("help.unlink_key") => t("help.unlink_value") }
         | 
| 126 | 
            +
                  )
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                def define_static_routes
         | 
| 130 | 
            +
                  self.class.route(
         | 
| 131 | 
            +
                    %r{^karma\s+worst},
         | 
| 132 | 
            +
                    :list_worst,
         | 
| 133 | 
            +
                    command: true,
         | 
| 134 | 
            +
                    help: { t("help.list_worst_key") => t("help.list_worst_value") }
         | 
| 135 | 
            +
                  )
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                  self.class.route(
         | 
| 138 | 
            +
                    %r{^karma\s+best},
         | 
| 139 | 
            +
                    :list_best,
         | 
| 140 | 
            +
                    command: true,
         | 
| 141 | 
            +
                    help: { t("help.list_best_key") => t("help.list_best_value") }
         | 
| 142 | 
            +
                  )
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  self.class.route(
         | 
| 145 | 
            +
                    %r{^karma\s+modified\s+.+},
         | 
| 146 | 
            +
                    :modified,
         | 
| 147 | 
            +
                    command: true,
         | 
| 148 | 
            +
                    help: { t("help.modified_key") => t("help.modified_value") }
         | 
| 149 | 
            +
                  )
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  self.class.route(
         | 
| 152 | 
            +
                    %r{^karma\s+delete},
         | 
| 153 | 
            +
                    :delete,
         | 
| 154 | 
            +
                    command: true,
         | 
| 155 | 
            +
                    restrict_to: :karma_admins,
         | 
| 156 | 
            +
                    help: { t("help.delete_key") => t("help.delete_value") }
         | 
| 157 | 
            +
                  )
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  self.class.route(%r{^karma\s*$}, :list_best, command: true)
         | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                def determine_list_count(response)
         | 
| 163 | 
            +
                  n = (response.args[1] || 5).to_i - 1
         | 
| 164 | 
            +
                  n = 25 if n > 25
         | 
| 165 | 
            +
                  n
         | 
| 166 | 
            +
                end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                def get_term(term)
         | 
| 169 | 
            +
                  Term.new(robot, term)
         | 
| 170 | 
            +
                end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                def list(response, method_name)
         | 
| 173 | 
            +
                  terms_and_scores = Term.public_send(method_name, robot, determine_list_count(response))
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  output = terms_and_scores.each_with_index.map do |term_and_score, index|
         | 
| 176 | 
            +
                    "#{index + 1}. #{term_and_score[0]} (#{term_and_score[1].to_i})"
         | 
| 177 | 
            +
                  end.join("\n")
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  if output.empty?
         | 
| 180 | 
            +
                    response.reply t("no_terms")
         | 
| 181 | 
            +
                  else
         | 
| 182 | 
            +
                    response.reply output
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                def modify(response, method_name)
         | 
| 187 | 
            +
                  user = response.user
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  output = response.matches.map do |match|
         | 
| 190 | 
            +
                    get_term(match[0]).public_send(method_name, user)
         | 
| 191 | 
            +
                  end
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                  response.reply output.join("; ")
         | 
| 194 | 
            +
                end
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                # To ensure that constructs like foo++bar or foo--bar (the latter is
         | 
| 197 | 
            +
                # common in some URL generation schemes) do not cause errant karma
         | 
| 198 | 
            +
                # modifications, force karma tokens be followed by whitespace (in a zero-
         | 
| 199 | 
            +
                # width, look-ahead operator) or the end of the string.
         | 
| 200 | 
            +
                def token_terminator
         | 
| 201 | 
            +
                  %r{(?:(?=\s)|$)}
         | 
| 202 | 
            +
                end
         | 
| 203 | 
            +
              end
         | 
| 204 | 
            +
            end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
            Lita.register_handler(Lita::Handlers::Karma::Chat)
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            module Lita::Handlers::Karma
         | 
| 2 | 
            +
              class Config < Lita::Handler
         | 
| 3 | 
            +
                namespace "karma"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                default_term_normalizer = proc do |term|
         | 
| 6 | 
            +
                  term.to_s.downcase.strip
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                config :cooldown, types: [Integer, nil], default: 300
         | 
| 10 | 
            +
                config :link_karma_threshold, types: [Integer, nil], default: 10
         | 
| 11 | 
            +
                config :term_pattern, type: Regexp, default: /[\[\]\p{Word}\._|\{\}]{2,}/
         | 
| 12 | 
            +
                config :term_normalizer, default: default_term_normalizer do
         | 
| 13 | 
            +
                  validate do |value|
         | 
| 14 | 
            +
                    t("callable_required") unless value.respond_to?(:call)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            Lita.register_handler(Lita::Handlers::Karma::Config)
         | 
| @@ -0,0 +1,152 @@ | |
| 1 | 
            +
             module Lita::Handlers::Karma
         | 
| 2 | 
            +
              class Term
         | 
| 3 | 
            +
                include Lita::Handler::Common
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                namespace "karma"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                attr_reader :term
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                class << self
         | 
| 10 | 
            +
                  def list_best(robot, n = 5)
         | 
| 11 | 
            +
                    list(:zrevrange, robot, n)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def list_worst(robot, n = 5)
         | 
| 15 | 
            +
                    list(:zrange, robot, n)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  private
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def list(redis_command, robot, n)
         | 
| 21 | 
            +
                    n = 24 if n > 24
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    handler = new(robot, '', normalize: false)
         | 
| 24 | 
            +
                    handler.redis.public_send(redis_command, "terms", 0, n, with_scores: true)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def initialize(robot, term, normalize: true)
         | 
| 29 | 
            +
                  super(robot)
         | 
| 30 | 
            +
                  @term = normalize ? normalize_term(term) : term
         | 
| 31 | 
            +
                  @link_cache = {}
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def check
         | 
| 35 | 
            +
                  string = "#{self}: #{total_score}"
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  unless links_with_scores.empty?
         | 
| 38 | 
            +
                    link_text = links_with_scores.map { |term, score| "#{term}: #{score}" }.join(", ")
         | 
| 39 | 
            +
                    string << " (#{own_score}), #{t("linked_to")}: #{link_text}"
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  string
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def decrement(user)
         | 
| 46 | 
            +
                  modify(user, -1)
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def delete
         | 
| 50 | 
            +
                  redis.zrem("terms", to_s)
         | 
| 51 | 
            +
                  redis.del("modified:#{self}")
         | 
| 52 | 
            +
                  redis.del("links:#{self}")
         | 
| 53 | 
            +
                  redis.smembers("linked_to:#{self}").each do |key|
         | 
| 54 | 
            +
                    redis.srem("links:#{key}", to_s)
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                  redis.del("linked_to:#{self}")
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def eql?(other)
         | 
| 60 | 
            +
                  term.eql?(other.term)
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def hash
         | 
| 64 | 
            +
                  term.hash
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def increment(user)
         | 
| 68 | 
            +
                  modify(user, 1)
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def link(other)
         | 
| 72 | 
            +
                  if config.link_karma_threshold
         | 
| 73 | 
            +
                    threshold = config.link_karma_threshold.abs
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    if own_score.abs < threshold || other.own_score.abs < threshold
         | 
| 76 | 
            +
                      return threshold
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  redis.sadd("links:#{self}", other.to_s) && redis.sadd("linked_to:#{other}", to_s)
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def links
         | 
| 84 | 
            +
                  @links ||= begin
         | 
| 85 | 
            +
                    redis.smembers("links:#{self}").each do |term|
         | 
| 86 | 
            +
                      linked_term = self.class.new(robot, term)
         | 
| 87 | 
            +
                      @link_cache[linked_term.term] = linked_term
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def links_with_scores
         | 
| 93 | 
            +
                  @links_with_scores ||= begin
         | 
| 94 | 
            +
                    {}.tap do |h|
         | 
| 95 | 
            +
                      links.each do |link|
         | 
| 96 | 
            +
                        h[link] = @link_cache[link].own_score
         | 
| 97 | 
            +
                      end
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
                end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                def modified
         | 
| 103 | 
            +
                  redis.smembers("modified:#{term}").map do |user_id|
         | 
| 104 | 
            +
                    Lita::User.find_by_id(user_id)
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                def own_score
         | 
| 109 | 
            +
                  @own_score ||= redis.zscore("terms", term).to_i
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                def to_s
         | 
| 113 | 
            +
                  term
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                def total_score
         | 
| 117 | 
            +
                  @total_score ||= begin
         | 
| 118 | 
            +
                    links.inject(own_score) do |memo, linked_term|
         | 
| 119 | 
            +
                      memo + @link_cache[linked_term].own_score
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                def unlink(other)
         | 
| 125 | 
            +
                  redis.srem("links:#{self}", other.to_s) && redis.srem("linked_to:#{other}", to_s)
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                private
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def modify(user, delta)
         | 
| 131 | 
            +
                  ttl = redis.ttl("cooldown:#{user.id}:#{term}")
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  if ttl > 0
         | 
| 134 | 
            +
                    t("cooling_down", term: self, ttl: ttl, count: ttl)
         | 
| 135 | 
            +
                  else
         | 
| 136 | 
            +
                    modify!(user, delta)
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                def modify!(user, delta)
         | 
| 141 | 
            +
                  user_id = user.id
         | 
| 142 | 
            +
                  redis.zincrby("terms", delta, term)
         | 
| 143 | 
            +
                  redis.sadd("modified:#{self}", user_id)
         | 
| 144 | 
            +
                  redis.setex("cooldown:#{user_id}:#{self}", config.cooldown, 1) if config.cooldown
         | 
| 145 | 
            +
                  check
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                def normalize_term(term)
         | 
| 149 | 
            +
                  config.term_normalizer.call(term)
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
              end
         | 
| 152 | 
            +
            end
         | 
    
        data/lita-karma.gemspec
    CHANGED
    
    | @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            Gem::Specification.new do |spec|
         | 
| 2 2 | 
             
              spec.name          = "lita-karma"
         | 
| 3 | 
            -
              spec.version       = "3.0. | 
| 3 | 
            +
              spec.version       = "3.0.1"
         | 
| 4 4 | 
             
              spec.authors       = ["Jimmy Cuadra"]
         | 
| 5 5 | 
             
              spec.email         = ["jimmy@jimmycuadra.com"]
         | 
| 6 6 | 
             
              spec.description   = %q{A Lita handler for tracking karma points for arbitrary terms.}
         | 
    
        data/locales/en.yml
    ADDED
    
    | @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            en:
         | 
| 2 | 
            +
              lita:
         | 
| 3 | 
            +
                handlers:
         | 
| 4 | 
            +
                  karma:
         | 
| 5 | 
            +
                    already_linked: "%{source} is already linked to %{target}."
         | 
| 6 | 
            +
                    already_unlinked: "%{source} is not linked to %{target}."
         | 
| 7 | 
            +
                    callable_required: must be a callable object
         | 
| 8 | 
            +
                    cooling_down:
         | 
| 9 | 
            +
                      one: "You cannot modify %{term} for another %{ttl} second."
         | 
| 10 | 
            +
                      other: "You cannot modify %{term} for another %{ttl} seconds."
         | 
| 11 | 
            +
                    delete_success: "%{term} has been deleted."
         | 
| 12 | 
            +
                    help:
         | 
| 13 | 
            +
                      increment_key: TERM++
         | 
| 14 | 
            +
                      increment_value: Increments TERM by one.
         | 
| 15 | 
            +
                      decrement_key: TERM--
         | 
| 16 | 
            +
                      decrement_value: Decrements TERM by one.
         | 
| 17 | 
            +
                      check_key: TERM~~
         | 
| 18 | 
            +
                      check_value: Shows the current karma of TERM.
         | 
| 19 | 
            +
                      link_key: TERM1 += TERM2
         | 
| 20 | 
            +
                      link_value: Links TERM2 to TERM1.
         | 
| 21 | 
            +
                      unlink_key: TERM1 -= TERM2
         | 
| 22 | 
            +
                      unlink_value: Unlinks TERM2 from TERM1.
         | 
| 23 | 
            +
                      list_worst_key: "karma worst [N]"
         | 
| 24 | 
            +
                      list_worst_value: Lists the bottom N terms by karma. N defaults to 5.
         | 
| 25 | 
            +
                      list_best_key: "karma best [N]"
         | 
| 26 | 
            +
                      list_best_value: Lists the top N terms by karma. N defaults to 5.
         | 
| 27 | 
            +
                      modified_key: karma modified TERM
         | 
| 28 | 
            +
                      modified_value: Lists the names of users who have upvoted or downvoted TERM.
         | 
| 29 | 
            +
                      delete_key: karma delete TERM
         | 
| 30 | 
            +
                      delete_value: >-
         | 
| 31 | 
            +
                        Permanently removes TERM and all its link information. TERM is matched
         | 
| 32 | 
            +
                        exactly as typed and does not adhere to the usual pattern for terms.
         | 
| 33 | 
            +
                    link_success: "%{source} has been linked to %{target}."
         | 
| 34 | 
            +
                    linked_to: linked to
         | 
| 35 | 
            +
                    never_modified: "%{term} has never been modified."
         | 
| 36 | 
            +
                    no_terms: There are no terms being tracked yet.
         | 
| 37 | 
            +
                    threshold_not_satisfied: >-
         | 
| 38 | 
            +
                      Terms must have less than or equal to -%{threshold} or greater than or equal to
         | 
| 39 | 
            +
                      %{threshold} karma to be linked or linked to.
         | 
| 40 | 
            +
                    unlink_success: "%{source} has been unlinked from %{target}."
         | 
| @@ -1,8 +1,10 @@ | |
| 1 1 | 
             
            require "spec_helper"
         | 
| 2 2 |  | 
| 3 | 
            -
            describe Lita::Handlers::Karma, lita_handler: true do
         | 
| 3 | 
            +
            describe Lita::Handlers::Karma::Chat, lita_handler: true do
         | 
| 4 4 | 
             
              let(:payload) { double("payload") }
         | 
| 5 5 |  | 
| 6 | 
            +
              prepend_before { registry.register_handler(Lita::Handlers::Karma::Config) }
         | 
| 7 | 
            +
             | 
| 6 8 | 
             
              before do
         | 
| 7 9 | 
             
                registry.config.handlers.karma.cooldown = nil
         | 
| 8 10 | 
             
                registry.config.handlers.karma.link_karma_threshold = nil
         | 
| @@ -12,35 +14,28 @@ describe Lita::Handlers::Karma, lita_handler: true do | |
| 12 14 |  | 
| 13 15 | 
             
              it { is_expected.to route("foo++").to(:increment) }
         | 
| 14 16 | 
             
              it { is_expected.to route("foo--").to(:decrement) }
         | 
| 17 | 
            +
              it { is_expected.to route("foo++ bar").to(:increment) }
         | 
| 18 | 
            +
              it { is_expected.to route("foo-- bar").to(:decrement) }
         | 
| 15 19 | 
             
              it { is_expected.to route("foo~~").to(:check) }
         | 
| 16 20 | 
             
              it { is_expected.to route_command("karma best").to(:list_best) }
         | 
| 17 21 | 
             
              it { is_expected.to route_command("karma worst").to(:list_worst) }
         | 
| 18 | 
            -
              it { is_expected.to route_command("karma modified").to(:modified) }
         | 
| 22 | 
            +
              it { is_expected.to route_command("karma modified foo").to(:modified) }
         | 
| 19 23 | 
             
              it do
         | 
| 20 24 | 
             
                is_expected.to route_command("karma delete").with_authorization_for(:karma_admins).to(:delete)
         | 
| 21 25 | 
             
              end
         | 
| 22 26 | 
             
              it { is_expected.to route_command("karma").to(:list_best) }
         | 
| 23 27 | 
             
              it { is_expected.to route_command("foo += bar").to(:link) }
         | 
| 28 | 
            +
              it { is_expected.to route_command("foo += bar++").to(:link) }
         | 
| 29 | 
            +
              it { is_expected.to route_command("foo += bar--").to(:link) }
         | 
| 30 | 
            +
              it { is_expected.to route_command("foo += bar~~").to(:link) }
         | 
| 24 31 | 
             
              it { is_expected.to route_command("foo -= bar").to(:unlink) }
         | 
| 32 | 
            +
              it { is_expected.to route_command("foo -= bar++").to(:unlink) }
         | 
| 33 | 
            +
              it { is_expected.to route_command("foo -= bar--").to(:unlink) }
         | 
| 34 | 
            +
              it { is_expected.to route_command("foo -= bar~~").to(:unlink) }
         | 
| 25 35 | 
             
              it { is_expected.not_to route("+++++").to(:increment) }
         | 
| 26 36 | 
             
              it { is_expected.not_to route("-----").to(:decrement) }
         | 
| 27 | 
            -
             | 
| 28 | 
            -
               | 
| 29 | 
            -
                before { subject.redis.flushdb }
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                it "adds reverse link data for all linked terms" do
         | 
| 32 | 
            -
                  subject.redis.sadd("links:foo", ["bar", "baz"])
         | 
| 33 | 
            -
                  subject.upgrade_data(payload)
         | 
| 34 | 
            -
                  expect(subject.redis.sismember("linked_to:bar", "foo")).to be(true)
         | 
| 35 | 
            -
                  expect(subject.redis.sismember("linked_to:baz", "foo")).to be(true)
         | 
| 36 | 
            -
                end
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                it "skips the update if it's already been done" do
         | 
| 39 | 
            -
                  expect(subject.redis).to receive(:keys).once.and_return([])
         | 
| 40 | 
            -
                  subject.upgrade_data(payload)
         | 
| 41 | 
            -
                  subject.upgrade_data(payload)
         | 
| 42 | 
            -
                end
         | 
| 43 | 
            -
              end
         | 
| 37 | 
            +
              it { is_expected.not_to route("foo++bar").to(:increment) }
         | 
| 38 | 
            +
              it { is_expected.not_to route("foo--bar").to(:decrement) }
         | 
| 44 39 |  | 
| 45 40 | 
             
              describe "#increment" do
         | 
| 46 41 | 
             
                it "increases the term's score by one and says the new score" do
         | 
| @@ -50,7 +45,7 @@ describe Lita::Handlers::Karma, lita_handler: true do | |
| 50 45 |  | 
| 51 46 | 
             
                it "matches multiple terms in one message" do
         | 
| 52 47 | 
             
                  send_message("foo++ bar++")
         | 
| 53 | 
            -
                  expect(replies).to eq( | 
| 48 | 
            +
                  expect(replies.last).to eq("foo: 1; bar: 1")
         | 
| 54 49 | 
             
                end
         | 
| 55 50 |  | 
| 56 51 | 
             
                it "doesn't start from zero if the term already has a positive score" do
         | 
| @@ -86,7 +81,7 @@ describe Lita::Handlers::Karma, lita_handler: true do | |
| 86 81 |  | 
| 87 82 | 
             
                it "matches multiple terms in one message" do
         | 
| 88 83 | 
             
                  send_message("foo-- bar--")
         | 
| 89 | 
            -
                  expect(replies).to eq( | 
| 84 | 
            +
                  expect(replies.last).to eq("foo: -1; bar: -1")
         | 
| 90 85 | 
             
                end
         | 
| 91 86 |  | 
| 92 87 | 
             
                it "doesn't start from zero if the term already has a positive score" do
         | 
| @@ -111,7 +106,12 @@ describe Lita::Handlers::Karma, lita_handler: true do | |
| 111 106 |  | 
| 112 107 | 
             
                it "matches multiple terms in one message" do
         | 
| 113 108 | 
             
                  send_message("foo~~ bar~~")
         | 
| 114 | 
            -
                  expect(replies).to eq( | 
| 109 | 
            +
                  expect(replies.last).to eq("foo: 0; bar: 0")
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                it "doesn't match the same term multiple times in one message" do
         | 
| 113 | 
            +
                  send_message("foo~~ foo~~")
         | 
| 114 | 
            +
                  expect(replies.size).to eq(1)
         | 
| 115 115 | 
             
                end
         | 
| 116 116 | 
             
              end
         | 
| 117 117 |  | 
| @@ -244,26 +244,18 @@ MSG | |
| 244 244 | 
             
              end
         | 
| 245 245 |  | 
| 246 246 | 
             
              describe "#modified" do
         | 
| 247 | 
            -
                it "replies with the required format if a term is not provided" do
         | 
| 248 | 
            -
                  send_command("karma modified")
         | 
| 249 | 
            -
                  expect(replies.last).to match(/^Format:/)
         | 
| 250 | 
            -
                end
         | 
| 251 | 
            -
             | 
| 252 | 
            -
                it "replies with the required format if the term is an empty string" do
         | 
| 253 | 
            -
                  send_command("karma modified '   '")
         | 
| 254 | 
            -
                  expect(replies.last).to match(/^Format:/)
         | 
| 255 | 
            -
                end
         | 
| 256 | 
            -
             | 
| 257 247 | 
             
                it "replies with a message if the term hasn't been modified" do
         | 
| 258 248 | 
             
                  send_command("karma modified foo")
         | 
| 259 249 | 
             
                  expect(replies.last).to match(/never been modified/)
         | 
| 260 250 | 
             
                end
         | 
| 261 251 |  | 
| 262 | 
            -
                it "lists users who have modified the given term" do
         | 
| 263 | 
            -
                   | 
| 264 | 
            -
                  send_message("foo++")
         | 
| 252 | 
            +
                it "lists users who have modified the given term in count order" do
         | 
| 253 | 
            +
                  other_user = Lita::User.create("2", name: "Other User")
         | 
| 254 | 
            +
                  send_message("foo++", as: user)
         | 
| 255 | 
            +
                  send_message("foo++", as: user)
         | 
| 256 | 
            +
                  send_message("foo++", as: other_user)
         | 
| 265 257 | 
             
                  send_command("karma modified foo")
         | 
| 266 | 
            -
                  expect(replies.last).to eq(user.name)
         | 
| 258 | 
            +
                  expect(replies.last).to eq("#{user.name}, #{other_user.name}")
         | 
| 267 259 | 
             
                end
         | 
| 268 260 | 
             
              end
         | 
| 269 261 |  | 
| @@ -280,11 +272,6 @@ MSG | |
| 280 272 | 
             
                  expect(replies.last).to eq("foo: 0")
         | 
| 281 273 | 
             
                end
         | 
| 282 274 |  | 
| 283 | 
            -
                it "replies with a warning if the term doesn't exist" do
         | 
| 284 | 
            -
                  send_command("karma delete foo")
         | 
| 285 | 
            -
                  expect(replies.last).to eq("foo does not exist.")
         | 
| 286 | 
            -
                end
         | 
| 287 | 
            -
             | 
| 288 275 | 
             
                it "matches terms exactly, including leading whitespace" do
         | 
| 289 276 | 
             
                  term = "  'foo bar* 'baz''/ :"
         | 
| 290 277 | 
             
                  subject.redis.zincrby("terms", 1, term)
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: lita-karma
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 3.0. | 
| 4 | 
            +
              version: 3.0.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Jimmy Cuadra
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2015-02-12 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: lita
         | 
| @@ -108,9 +108,12 @@ files: | |
| 108 108 | 
             
            - README.md
         | 
| 109 109 | 
             
            - Rakefile
         | 
| 110 110 | 
             
            - lib/lita-karma.rb
         | 
| 111 | 
            -
            - lib/lita/handlers/karma.rb
         | 
| 111 | 
            +
            - lib/lita/handlers/karma/chat.rb
         | 
| 112 | 
            +
            - lib/lita/handlers/karma/config.rb
         | 
| 113 | 
            +
            - lib/lita/handlers/karma/term.rb
         | 
| 112 114 | 
             
            - lita-karma.gemspec
         | 
| 113 | 
            -
            -  | 
| 115 | 
            +
            - locales/en.yml
         | 
| 116 | 
            +
            - spec/lita/handlers/karma/chat_spec.rb
         | 
| 114 117 | 
             
            - spec/spec_helper.rb
         | 
| 115 118 | 
             
            homepage: https://github.com/jimmycuadra/lita-karma
         | 
| 116 119 | 
             
            licenses:
         | 
| @@ -133,10 +136,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 133 136 | 
             
                  version: '0'
         | 
| 134 137 | 
             
            requirements: []
         | 
| 135 138 | 
             
            rubyforge_project: 
         | 
| 136 | 
            -
            rubygems_version: 2. | 
| 139 | 
            +
            rubygems_version: 2.4.5
         | 
| 137 140 | 
             
            signing_key: 
         | 
| 138 141 | 
             
            specification_version: 4
         | 
| 139 142 | 
             
            summary: A Lita handler for tracking karma points for arbitrary terms.
         | 
| 140 143 | 
             
            test_files:
         | 
| 141 | 
            -
            - spec/lita/handlers/ | 
| 144 | 
            +
            - spec/lita/handlers/karma/chat_spec.rb
         | 
| 142 145 | 
             
            - spec/spec_helper.rb
         | 
| 146 | 
            +
            has_rdoc: 
         | 
    
        data/lib/lita/handlers/karma.rb
    DELETED
    
    | @@ -1,315 +0,0 @@ | |
| 1 | 
            -
            require "lita"
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module Lita
         | 
| 4 | 
            -
              module Handlers
         | 
| 5 | 
            -
                # Tracks karma points for arbitrary terms.
         | 
| 6 | 
            -
                class Karma < Handler
         | 
| 7 | 
            -
                  config :cooldown, types: [Integer, nil], default: 300
         | 
| 8 | 
            -
                  config :link_karma_threshold, types: [Integer, nil], default: 10
         | 
| 9 | 
            -
                  config :term_pattern, type: Regexp, default: /[\[\]\p{Word}\._|\{\}]{2,}/
         | 
| 10 | 
            -
                  config :term_normalizer do
         | 
| 11 | 
            -
                    validate do |value|
         | 
| 12 | 
            -
                      "must be a callable object" unless value.respond_to?(:call)
         | 
| 13 | 
            -
                    end
         | 
| 14 | 
            -
                  end
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                  on :loaded, :upgrade_data
         | 
| 17 | 
            -
                  on :loaded, :define_routes
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                  def upgrade_data(payload)
         | 
| 20 | 
            -
                    return if redis.exists("support:reverse_links")
         | 
| 21 | 
            -
                    redis.keys("links:*").each do |key|
         | 
| 22 | 
            -
                      term = key.sub(/^links:/, "")
         | 
| 23 | 
            -
                      redis.smembers(key).each do |link|
         | 
| 24 | 
            -
                        redis.sadd("linked_to:#{link}", term)
         | 
| 25 | 
            -
                      end
         | 
| 26 | 
            -
                    end
         | 
| 27 | 
            -
                    redis.incr("support:reverse_links")
         | 
| 28 | 
            -
                  end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                  def define_routes(payload)
         | 
| 31 | 
            -
                    define_static_routes
         | 
| 32 | 
            -
                    define_dynamic_routes(config.term_pattern.source)
         | 
| 33 | 
            -
                  end
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                  def increment(response)
         | 
| 36 | 
            -
                    modify(response, 1)
         | 
| 37 | 
            -
                  end
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                  def decrement(response)
         | 
| 40 | 
            -
                    modify(response, -1)
         | 
| 41 | 
            -
                  end
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                  def check(response)
         | 
| 44 | 
            -
                    output = []
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                    response.matches.each do |match|
         | 
| 47 | 
            -
                      term = normalize_term(match[0])
         | 
| 48 | 
            -
                      total_score, own_score, links = scores_for(term)
         | 
| 49 | 
            -
             | 
| 50 | 
            -
                      string = "#{term}: #{total_score}"
         | 
| 51 | 
            -
                      unless links.empty?
         | 
| 52 | 
            -
                        string << " (#{own_score}), linked to: #{links.join(", ")}"
         | 
| 53 | 
            -
                      end
         | 
| 54 | 
            -
                      output << string
         | 
| 55 | 
            -
                    end
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                    response.reply *output
         | 
| 58 | 
            -
                  end
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                  def list_best(response)
         | 
| 61 | 
            -
                    list(response, :zrevrange)
         | 
| 62 | 
            -
                  end
         | 
| 63 | 
            -
             | 
| 64 | 
            -
                  def list_worst(response)
         | 
| 65 | 
            -
                    list(response, :zrange)
         | 
| 66 | 
            -
                  end
         | 
| 67 | 
            -
             | 
| 68 | 
            -
                  def link(response)
         | 
| 69 | 
            -
                    response.matches.each do |match|
         | 
| 70 | 
            -
                      term1, term2 = normalize_term(match[0]), normalize_term(match[1])
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                      if config.link_karma_threshold
         | 
| 73 | 
            -
                        threshold = config.link_karma_threshold.abs
         | 
| 74 | 
            -
             | 
| 75 | 
            -
                        _total_score, term2_score, _links = scores_for(term2)
         | 
| 76 | 
            -
                        _total_score, term1_score, _links = scores_for(term1)
         | 
| 77 | 
            -
             | 
| 78 | 
            -
                        if term1_score.abs < threshold || term2_score.abs < threshold
         | 
| 79 | 
            -
                          response.reply "Terms must have less than -#{threshold} or more than #{threshold} karma to be linked or linked to."
         | 
| 80 | 
            -
                          return
         | 
| 81 | 
            -
                        end
         | 
| 82 | 
            -
                      end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                      if redis.sadd("links:#{term1}", term2)
         | 
| 85 | 
            -
                        redis.sadd("linked_to:#{term2}", term1)
         | 
| 86 | 
            -
                        response.reply "#{term2} has been linked to #{term1}."
         | 
| 87 | 
            -
                      else
         | 
| 88 | 
            -
                        response.reply "#{term2} is already linked to #{term1}."
         | 
| 89 | 
            -
                      end
         | 
| 90 | 
            -
                    end
         | 
| 91 | 
            -
                  end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                  def unlink(response)
         | 
| 94 | 
            -
                    response.matches.each do |match|
         | 
| 95 | 
            -
                      term1, term2 = normalize_term(match[0]), normalize_term(match[1])
         | 
| 96 | 
            -
             | 
| 97 | 
            -
                      if redis.srem("links:#{term1}", term2)
         | 
| 98 | 
            -
                        redis.srem("linked_to:#{term2}", term1)
         | 
| 99 | 
            -
                        response.reply "#{term2} has been unlinked from #{term1}."
         | 
| 100 | 
            -
                      else
         | 
| 101 | 
            -
                        response.reply "#{term2} is not linked to #{term1}."
         | 
| 102 | 
            -
                      end
         | 
| 103 | 
            -
                    end
         | 
| 104 | 
            -
                  end
         | 
| 105 | 
            -
             | 
| 106 | 
            -
                  def modified(response)
         | 
| 107 | 
            -
                    term = normalize_term(response.args[1])
         | 
| 108 | 
            -
             | 
| 109 | 
            -
                    if term.empty?
         | 
| 110 | 
            -
                      response.reply "Format: #{robot.name}: karma modified TERM"
         | 
| 111 | 
            -
                      return
         | 
| 112 | 
            -
                    end
         | 
| 113 | 
            -
             | 
| 114 | 
            -
                    user_ids = redis.smembers("modified:#{term}")
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                    if user_ids.empty?
         | 
| 117 | 
            -
                      response.reply "#{term} has never been modified."
         | 
| 118 | 
            -
                    else
         | 
| 119 | 
            -
                      output = user_ids.map do |id|
         | 
| 120 | 
            -
                        User.find_by_id(id).name
         | 
| 121 | 
            -
                      end.join(", ")
         | 
| 122 | 
            -
                      response.reply output
         | 
| 123 | 
            -
                    end
         | 
| 124 | 
            -
                  end
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                  def delete(response)
         | 
| 127 | 
            -
                    term = response.message.body.sub(/^karma delete /, "")
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                    redis.del("modified:#{term}")
         | 
| 130 | 
            -
                    redis.del("links:#{term}")
         | 
| 131 | 
            -
                    redis.smembers("linked_to:#{term}").each do |key|
         | 
| 132 | 
            -
                      redis.srem("links:#{key}", term)
         | 
| 133 | 
            -
                    end
         | 
| 134 | 
            -
                    redis.del("linked_to:#{term}")
         | 
| 135 | 
            -
             | 
| 136 | 
            -
                    if redis.zrem("terms", term)
         | 
| 137 | 
            -
                      response.reply("#{term} has been deleted.")
         | 
| 138 | 
            -
                    else
         | 
| 139 | 
            -
                      response.reply("#{term} does not exist.")
         | 
| 140 | 
            -
                    end
         | 
| 141 | 
            -
                  end
         | 
| 142 | 
            -
             | 
| 143 | 
            -
                  private
         | 
| 144 | 
            -
             | 
| 145 | 
            -
                  def cooling_down?(term, user_id, response)
         | 
| 146 | 
            -
                    ttl = redis.ttl("cooldown:#{user_id}:#{term}")
         | 
| 147 | 
            -
             | 
| 148 | 
            -
                    if ttl >= 0
         | 
| 149 | 
            -
                      cooldown_message =
         | 
| 150 | 
            -
                        "You cannot modify #{term} for another #{ttl} second"
         | 
| 151 | 
            -
                      cooldown_message << (ttl == 1 ? "." : "s.")
         | 
| 152 | 
            -
                      response.reply cooldown_message
         | 
| 153 | 
            -
                      return true
         | 
| 154 | 
            -
                    else
         | 
| 155 | 
            -
                      return false
         | 
| 156 | 
            -
                    end
         | 
| 157 | 
            -
                  end
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                  def define_dynamic_routes(pattern)
         | 
| 160 | 
            -
                    self.class.route(
         | 
| 161 | 
            -
                      %r{(#{pattern})\+\+},
         | 
| 162 | 
            -
                      :increment,
         | 
| 163 | 
            -
                      help: { "TERM++" => "Increments TERM by one." }
         | 
| 164 | 
            -
                    )
         | 
| 165 | 
            -
             | 
| 166 | 
            -
                    self.class.route(
         | 
| 167 | 
            -
                      %r{(#{pattern})\-\-},
         | 
| 168 | 
            -
                      :decrement,
         | 
| 169 | 
            -
                      help: { "TERM--" => "Decrements TERM by one." }
         | 
| 170 | 
            -
                    )
         | 
| 171 | 
            -
             | 
| 172 | 
            -
                    self.class.route(
         | 
| 173 | 
            -
                      %r{(#{pattern})~~},
         | 
| 174 | 
            -
                      :check,
         | 
| 175 | 
            -
                      help: { "TERM~~" => "Shows the current karma of TERM." }
         | 
| 176 | 
            -
                    )
         | 
| 177 | 
            -
             | 
| 178 | 
            -
                    self.class.route(
         | 
| 179 | 
            -
                      %r{^(#{pattern})\s*\+=\s*(#{pattern})},
         | 
| 180 | 
            -
                      :link,
         | 
| 181 | 
            -
                      command: true,
         | 
| 182 | 
            -
                      help: {
         | 
| 183 | 
            -
                        "TERM1 += TERM2" => <<-HELP.chomp
         | 
| 184 | 
            -
            Links TERM2 to TERM1. TERM1's karma will then be displayed as the sum of its \
         | 
| 185 | 
            -
            own and TERM2's karma.
         | 
| 186 | 
            -
            HELP
         | 
| 187 | 
            -
                      }
         | 
| 188 | 
            -
                    )
         | 
| 189 | 
            -
             | 
| 190 | 
            -
                    self.class.route(
         | 
| 191 | 
            -
                      %r{^(#{pattern})\s*-=\s*(#{pattern})},
         | 
| 192 | 
            -
                      :unlink,
         | 
| 193 | 
            -
                      command: true,
         | 
| 194 | 
            -
                      help: {
         | 
| 195 | 
            -
                        "TERM1 -= TERM2" => <<-HELP.chomp
         | 
| 196 | 
            -
            Unlinks TERM2 from TERM1. TERM1's karma will no longer be displayed as the sum \
         | 
| 197 | 
            -
            of its own and TERM2's karma.
         | 
| 198 | 
            -
            HELP
         | 
| 199 | 
            -
                      }
         | 
| 200 | 
            -
                    )
         | 
| 201 | 
            -
                  end
         | 
| 202 | 
            -
             | 
| 203 | 
            -
                  def define_static_routes
         | 
| 204 | 
            -
                    self.class.route(
         | 
| 205 | 
            -
                      %r{^karma\s+worst},
         | 
| 206 | 
            -
                      :list_worst,
         | 
| 207 | 
            -
                      command: true,
         | 
| 208 | 
            -
                      help: {
         | 
| 209 | 
            -
                        "karma worst [N]" => <<-HELP.chomp
         | 
| 210 | 
            -
            Lists the bottom N terms by karma. N defaults to 5.
         | 
| 211 | 
            -
            HELP
         | 
| 212 | 
            -
                      }
         | 
| 213 | 
            -
                    )
         | 
| 214 | 
            -
             | 
| 215 | 
            -
                    self.class.route(
         | 
| 216 | 
            -
                      %r{^karma\s+best},
         | 
| 217 | 
            -
                      :list_best,
         | 
| 218 | 
            -
                      command: true,
         | 
| 219 | 
            -
                      help: {
         | 
| 220 | 
            -
                        "karma best [N]" => <<-HELP.chomp
         | 
| 221 | 
            -
            Lists the top N terms by karma. N defaults to 5.
         | 
| 222 | 
            -
            HELP
         | 
| 223 | 
            -
                      }
         | 
| 224 | 
            -
                    )
         | 
| 225 | 
            -
             | 
| 226 | 
            -
                    self.class.route(
         | 
| 227 | 
            -
                      %r{^karma\s+modified},
         | 
| 228 | 
            -
                      :modified,
         | 
| 229 | 
            -
                      command: true,
         | 
| 230 | 
            -
                      help: {
         | 
| 231 | 
            -
                        "karma modified TERM" => <<-HELP.chomp
         | 
| 232 | 
            -
            Lists the names of users who have upvoted or downvoted TERM.
         | 
| 233 | 
            -
            HELP
         | 
| 234 | 
            -
                      }
         | 
| 235 | 
            -
                    )
         | 
| 236 | 
            -
             | 
| 237 | 
            -
                    self.class.route(
         | 
| 238 | 
            -
                      %r{^karma\s+delete},
         | 
| 239 | 
            -
                      :delete,
         | 
| 240 | 
            -
                      command: true,
         | 
| 241 | 
            -
                      restrict_to: :karma_admins,
         | 
| 242 | 
            -
                      help: {
         | 
| 243 | 
            -
                        "karma delete TERM" => <<-HELP.chomp
         | 
| 244 | 
            -
            Permanently removes TERM and all its links. TERM is matched exactly as typed \
         | 
| 245 | 
            -
            and does not adhere to the usual pattern for terms.
         | 
| 246 | 
            -
            HELP
         | 
| 247 | 
            -
                      }
         | 
| 248 | 
            -
                    )
         | 
| 249 | 
            -
             | 
| 250 | 
            -
                    self.class.route(%r{^karma\s*$}, :list_best, command: true)
         | 
| 251 | 
            -
                  end
         | 
| 252 | 
            -
             | 
| 253 | 
            -
                  def modify(response, delta)
         | 
| 254 | 
            -
                    response.matches.each do |match|
         | 
| 255 | 
            -
                      term = normalize_term(match[0])
         | 
| 256 | 
            -
                      user_id = response.user.id
         | 
| 257 | 
            -
             | 
| 258 | 
            -
                      return if cooling_down?(term, user_id, response)
         | 
| 259 | 
            -
             | 
| 260 | 
            -
                      redis.zincrby("terms", delta, term)
         | 
| 261 | 
            -
                      redis.sadd("modified:#{term}", user_id)
         | 
| 262 | 
            -
                      set_cooldown(term, response.user.id)
         | 
| 263 | 
            -
                    end
         | 
| 264 | 
            -
             | 
| 265 | 
            -
                    check(response)
         | 
| 266 | 
            -
                  end
         | 
| 267 | 
            -
             | 
| 268 | 
            -
                  def normalize_term(term)
         | 
| 269 | 
            -
                    if config.term_normalizer
         | 
| 270 | 
            -
                      config.term_normalizer.call(term)
         | 
| 271 | 
            -
                    else
         | 
| 272 | 
            -
                      term.to_s.downcase.strip
         | 
| 273 | 
            -
                    end
         | 
| 274 | 
            -
                  end
         | 
| 275 | 
            -
             | 
| 276 | 
            -
                  def list(response, redis_command)
         | 
| 277 | 
            -
                    n = (response.args[1] || 5).to_i - 1
         | 
| 278 | 
            -
                    n = 25 if n > 25
         | 
| 279 | 
            -
             | 
| 280 | 
            -
                    terms_scores = redis.public_send(
         | 
| 281 | 
            -
                      redis_command, "terms", 0, n, with_scores: true
         | 
| 282 | 
            -
                    )
         | 
| 283 | 
            -
             | 
| 284 | 
            -
                    output = terms_scores.each_with_index.map do |term_score, index|
         | 
| 285 | 
            -
                      "#{index + 1}. #{term_score[0]} (#{term_score[1].to_i})"
         | 
| 286 | 
            -
                    end.join("\n")
         | 
| 287 | 
            -
             | 
| 288 | 
            -
                    if output.length == 0
         | 
| 289 | 
            -
                      response.reply "There are no terms being tracked yet."
         | 
| 290 | 
            -
                    else
         | 
| 291 | 
            -
                      response.reply output
         | 
| 292 | 
            -
                    end
         | 
| 293 | 
            -
                  end
         | 
| 294 | 
            -
             | 
| 295 | 
            -
                  def scores_for(term)
         | 
| 296 | 
            -
                    own_score = total_score = redis.zscore("terms", term).to_i
         | 
| 297 | 
            -
                    links = []
         | 
| 298 | 
            -
             | 
| 299 | 
            -
                    redis.smembers("links:#{term}").each do |link|
         | 
| 300 | 
            -
                      link_score = redis.zscore("terms", link).to_i
         | 
| 301 | 
            -
                      links << "#{link}: #{link_score}"
         | 
| 302 | 
            -
                      total_score += link_score
         | 
| 303 | 
            -
                    end
         | 
| 304 | 
            -
             | 
| 305 | 
            -
                    [total_score, own_score, links]
         | 
| 306 | 
            -
                  end
         | 
| 307 | 
            -
             | 
| 308 | 
            -
                  def set_cooldown(term, user_id)
         | 
| 309 | 
            -
                    redis.setex("cooldown:#{user_id}:#{term}", config.cooldown.to_i, 1) if config.cooldown
         | 
| 310 | 
            -
                  end
         | 
| 311 | 
            -
                end
         | 
| 312 | 
            -
             | 
| 313 | 
            -
                Lita.register_handler(Karma)
         | 
| 314 | 
            -
              end
         | 
| 315 | 
            -
            end
         |