module Lita # A user in the chat service. Persisted in Redis. class User class << self # The +Redis::Namespace+ for user persistence. # @return [Redis::Namespace] The Redis connection. def redis @redis ||= Redis::Namespace.new("users", redis: Lita.redis) end # Creates a new user with the given ID, or merges and saves supplied # metadata to an existing user with the given ID. # @param id [Integer, String] A unique identifier for the user. # @param metadata [Hash] An optional hash of metadata about the user. # @option metadata [String] name (id) The display name of the user. # @return [Lita::User] The user. def create(id, metadata = {}) existing_user = find_by_id(id) metadata = Util.stringify_keys(metadata) metadata = existing_user.metadata.merge(metadata) if existing_user user = new(id, metadata) user.save user end # Finds a user by ID. # @param id [Integer, String] The user's unique ID. # @return [Lita::User, nil] The user or +nil+ if no such user is known. def find_by_id(id) metadata = redis.hgetall("id:#{id}") new(id, metadata) if metadata.key?("name") end # Finds a user by mention name. # @param mention_name [String] The user's mention name. # @return [Lita::User, nil] The user or +nil+ if no such user is known. # @since 3.0.0 def find_by_mention_name(mention_name) id = redis.get("mention_name:#{mention_name}") find_by_id(id) if id end # Finds a user by display name. # @param name [String] The user's name. # @return [Lita::User, nil] The user or +nil+ if no such user is known. def find_by_name(name) id = redis.get("name:#{name}") find_by_id(id) if id end # Attempts to find a user with a name starting with the provided string. # @param name [String] The first characters in the user's name. # @return [Lita::User, nil] The user, or +nil+ if zero or greater than 1 matches were found. # @since 3.0.0 def find_by_partial_name(name) keys = redis.keys("name:#{name}*") if keys.length == 1 id = redis.get(keys.first) find_by_id(id) end end # Finds a user by ID, mention name, name, or partial name. # @param identifier [String] The user's ID, name, partial name, or mention name. # @return [Lita::User, nil] The user or +nil+ if no users were found. # @since 3.0.0 def fuzzy_find(identifier) find_by_id(identifier) || find_by_mention_name(identifier) || find_by_name(identifier) || find_by_partial_name(identifier) end end # The user's unique ID. # @return [String] The user's ID. attr_reader :id # A hash of arbitrary metadata about the user. # @return [Hash] The user's metadata. attr_reader :metadata # The user's name as displayed in the chat. # @return [String] The user's name. attr_reader :name # @param id [Integer, String] The user's unique ID. # @param metadata [Hash] Arbitrary user metadata. # @option metadata [String] name (id) The user's display name. def initialize(id, metadata = {}) @id = id.to_s @metadata = Util.stringify_keys(metadata) @name = @metadata["name"] || @id ensure_name_metadata_set end # The name used to "mention" the user in a group chat. # @return [String] The user's mention name. # @since 3.1.0 def mention_name metadata["mention_name"] || name end # Saves the user record to Redis, overwriting an previous data for the # current ID and user name. # @return [void] def save mention_name = metadata[:mention_name] || metadata["mention_name"] redis.pipelined do redis.hmset("id:#{id}", *metadata.to_a.flatten) redis.set("name:#{name}", id) redis.set("mention_name:#{mention_name}", id) if mention_name end end # Compares the user against another user object to determine equality. Users # are considered equal if they have the same ID and name. # @param other (Lita::User) The user to compare against. # @return [Boolean] True if users are equal, false otherwise. def ==(other) other.respond_to?(:id) && id == other.id && other.respond_to?(:name) && name == other.name end private # Ensure the user's metadata contains their name, to ensure their Redis hash contains at least # one value. It's not possible to store an empty hash key in Redis. def ensure_name_metadata_set username = metadata.delete("name") metadata["name"] = username || id end # The Redis connection for user persistence. def redis self.class.redis end end end