diff --git a/src/command.cr b/src/command.cr index a999726..23a844c 100644 --- a/src/command.cr +++ b/src/command.cr @@ -1,285 +1,352 @@ +enum BanType + Creation + Join +end + class Slash def handle_command @client.on_message_create do |msg| - # TODO figure out why this is broken... - unless msg.guild_id == @server - Log.debug { "Other server" } - next - end + m = handle_command(msg) - next unless @admins.includes?(msg.author.id) + @client.create_message(msg.channel_id, m, allowed_mentions: NO_MENTION) if m + end + end - cmd_string = @regex.match(msg.content).try &.["cmd"]? - next unless cmd_string + private def handle_command(msg) : String? + # TODO figure out why this is broken... + unless msg.guild_id == @server + Log.debug { "Other server" } + return + end - # TODO improve error handling below + return unless @admins.includes?(msg.author.id) - # Must not remove empty to preserve phrases as-is when re-joining - cmd = cmd_string.split(' ', remove_empty: false) - m = nil - case cmd[0] - when "ping" - m = "pong" - when "restart" - @client.create_message(msg.channel_id, "Restarting...") - exit - when "help" - m = <<-HELP - ``` - list - ping - [GROUP] - group add [name] [mode] - group delete [name] - [WORD] - word add [group] [phrase] - word delete [group] [phrase] - [WHITELIST] - whitelist add [group] [word] - whitelist delete [group] [word] - [PFP] - pfp add [name] [link/ID/mention] - pfp delete [name] - pfp scan [link/ID/mention] [?diff] - [SPAM] - lookup [phrase] - ``` - HELP - when "\u{1f4be}" # 💾 - stats = GC.stats - stats_string = String.build do |string| - string << "```cr\n" - string << "heap_size: " << (stats.heap_size / 1024.0 / 1024.0).round(2) << "MB\n" - string << "free_bytes: " << (stats.free_bytes / 1024.0 / 1024.0).round(2) << "MB\n" - string << "unmapped_bytes: " << (stats.unmapped_bytes / 1024.0 / 1024.0).round(2) << "MB\n" - string << "bytes_since_gc: " << (stats.bytes_since_gc / 1024.0 / 1024.0).round(2) << "MB\n" - string << "total_bytes: " << (stats.total_bytes / 1024.0 / 1024.0).round(2) << "MB\n" - string << "```" - end - stats_field = Discord::EmbedField.new( - "gc stats", - stats_string, - true) + cmd_string = @regex.match(msg.content).try &.["cmd"]? + return unless cmd_string - @client.create_message(msg.channel_id, - "**GC statistics**", - Discord::Embed.new( - fields: [stats_field])) - when "pfp" - case cmd[1]? - when "create", "add" - name = cmd[2]? - mention_string = cmd[3]? - if name && mention_string - if @dhash[name]? - m = "Already tracking with name `#{name}`" - else - link = parse_pfp_object(mention_string) - begin - hash = get_hash(link) - @dhash[name] = hash - write - m = "Tracking #{name}. *Launching scan of cache*" - spawn do - Fiber.yield - res, t = get_scan_results(hash) - if res.empty? - @client.create_message(msg.channel_id, "No matches found.") - else - post_scan_results(res, t, msg) - end + # TODO improve error handling below + + # Must not remove empty to preserve phrases as-is when re-joining + cmd = cmd_string.split(' ', remove_empty: false) + m = nil + case cmd[0] + when "ping" + return "pong" + when "restart" + @client.create_message(msg.channel_id, "Restarting...") + exit + when "help" + return <<-HELP + ``` + list + ping + [GROUP] + group add [name] [mode] + group delete [name] + [WORD] + word add [group] [phrase] + word delete [group] [phrase] + [WHITELIST] + whitelist add [group] [word] + whitelist delete [group] [word] + [PFP] + pfp add [name] [link/ID/mention] + pfp delete [name] + pfp scan [link/ID/mention] [?diff] + [SPAM] + lookup [phrase] + ``` + HELP + when "\u{1f4be}" # 💾 + stats = GC.stats + stats_string = String.build do |string| + string << "```cr\n" + string << "heap_size: " << (stats.heap_size / 1024.0 / 1024.0).round(2) << "MB\n" + string << "free_bytes: " << (stats.free_bytes / 1024.0 / 1024.0).round(2) << "MB\n" + string << "unmapped_bytes: " << (stats.unmapped_bytes / 1024.0 / 1024.0).round(2) << "MB\n" + string << "bytes_since_gc: " << (stats.bytes_since_gc / 1024.0 / 1024.0).round(2) << "MB\n" + string << "total_bytes: " << (stats.total_bytes / 1024.0 / 1024.0).round(2) << "MB\n" + string << "```" + end + stats_field = Discord::EmbedField.new( + "gc stats", + stats_string, + true) + + @client.create_message(msg.channel_id, + "**GC statistics**", + Discord::Embed.new( + fields: [stats_field])) + return + when "pfp" + case cmd[1]? + when "create", "add" + name = cmd[2]? + mention_string = cmd[3]? + if name && mention_string + if @dhash[name]? + return "Already tracking with name `#{name}`" + else + link = parse_pfp_object(mention_string) + begin + hash = get_hash(link) + @dhash[name] = hash + write + m = "Tracking #{name}. *Launching scan of cache*" + spawn do + Fiber.yield + res, t = get_scan_results(hash) + if res.empty? + @client.create_message(msg.channel_id, "No matches found.") + else + post_scan_results(res, t, msg) end - rescue - m = "Tried to get invalid image. Please provide valid mention, ID or link." end + return m + rescue + return "Tried to get invalid image. Please provide valid mention, ID or link." end - else - m = "Error. Syntax: pfp add [name] [link/ID/mention]" - end - when "delete", "remove" - name = cmd[2]? - if name - @dhash.delete(name) - write - m = "Deleted #{name}" - else - m = "Error. Syntax: pfp delete [name]" end - when "scan", "search" - mention_string = cmd[2]? - in_diff = cmd[3]? - if in_diff - diff = in_diff.to_i32? - else - diff = DIFF_FOR_PERFECT_MATCH - end - if diff.nil? - m = "Invalid diff provided. Syntax: `pfp scan [link] [diff]`" - elsif diff <= 50 - if mention_string - link = parse_pfp_object(mention_string) - if link - begin - hash = get_hash(link) - res, t = get_scan_results(hash, diff) - if res.empty? - m = "No results. Maybe try a higher diff? (Took #{t.total_milliseconds} ms)" - else - post_scan_results(res, t, msg) - m = nil # has it's own sending due to attached file - end - rescue - m = "Please provide a valid mention, ID or link. Error getting pfp." + else + return "Error. Syntax: pfp add [name] [link/ID/mention]" + end + when "delete", "remove" + name = cmd[2]? + if name + @dhash.delete(name) + write + return "Deleted #{name}" + else + return "Error. Syntax: pfp delete [name]" + end + when "scan", "search" + mention_string = cmd[2]? + in_diff = cmd[3]? + if in_diff + diff = in_diff.to_i32? + else + diff = DIFF_FOR_PERFECT_MATCH + end + if diff.nil? + return "Invalid diff provided. Syntax: `pfp scan [link] [diff]`" + elsif diff <= 50 + if mention_string + link = parse_pfp_object(mention_string) + if link + begin + hash = get_hash(link) + res, t = get_scan_results(hash, diff) + if res.empty? + return "No results. Maybe try a higher diff? (Took #{t.total_milliseconds} ms)" + else + post_scan_results(res, t, msg) + return # has it's own sending due to attached file end - else - m = "Error. Please provide link to scan" + rescue + return "Please provide a valid mention, ID or link. Error getting pfp." end + else + return "Error. Please provide link to scan" end - else - m = "Please try a lower diff." end - when "debug" - m = "Cached Pfp Hashes: #{@hashcache.storage.map { |_, x| x.size }.sum}" else - m = "Unknown or no Sub-Command given" + return "Please try a lower diff." end - when "group" - case cmd[1]? - when "create", "add" - # `group create [name] [mode]` - name = cmd[2]? - mode = cmd[3]? - if name && mode && @groups.has_key? name - m = "Group `#{name}` already exists" - elsif name && mode - @groups[name] = Group.new(name, Mode.parse(mode)) - write - m = "Successfully created group `#{name}` with mode `#{mode}`" - else - m = "Error. Syntax: `group add [name] [mode]`" - end - when "delete", "remove" - # `group delete [name]` - name = cmd[2]? - if name - @groups.delete(name) - write - m = "Successfully deleted group" - else - m = "Error. Syntax: `group delete [name]`" - end - # when "edit" + when "debug" + return "Cached Pfp Hashes: #{@hashcache.storage.map { |_, x| x.size }.sum}" + else + return "Unknown or no Sub-Command given" + end + when "group" + case cmd[1]? + when "create", "add" + # `group create [name] [mode]` + name = cmd[2]? + mode = cmd[3]? + if name && mode && @groups.has_key? name + return "Group `#{name}` already exists" + elsif name && mode + @groups[name] = Group.new(name, Mode.parse(mode)) + write + return "Successfully created group `#{name}` with mode `#{mode}`" else - m = "Unknown or no Sub-Command given" + return "Error. Syntax: `group add [name] [mode]`" end - when "word" - case cmd[1]? - when "add" - # `word add [group] [phrase]` - group = cmd[2]? - phrase_existence = cmd[3]? + when "delete", "remove" + # `group delete [name]` + name = cmd[2]? + if name + @groups.delete(name) + write + return "Successfully deleted group" + else + return "Error. Syntax: `group delete [name]`" + end + # when "edit" + else + return "Unknown or no Sub-Command given" + end + when "word" + case cmd[1]? + when "add" + # `word add [group] [phrase]` + group = cmd[2]? + phrase_existence = cmd[3]? - if group && !@groups.has_key? group - m = "Group does not exist" - elsif group && phrase_existence && @groups[group].add_phrase(cmd[3..].join(" ")) - write - m = "Successfully added `#{cmd[3..].join(" ")}` to group `#{group}`." - else - m = "Unable to add phrase. Syntax: `word add [group] [phrase]`" - end - when "delete", "remove" - # `word delete [group] [phrase]` - group = cmd[2]? - phrase_existence = cmd[3]? + if group && !@groups.has_key? group + return "Group does not exist" + elsif group && phrase_existence && @groups[group].add_phrase(cmd[3..].join(" ")) + write + return "Successfully added `#{cmd[3..].join(" ")}` to group `#{group}`." + else + return "Unable to add phrase. Syntax: `word add [group] [phrase]`" + end + when "delete", "remove" + # `word delete [group] [phrase]` + group = cmd[2]? + phrase_existence = cmd[3]? - if group && !@groups.has_key? group - m = "Group does not exist" - elsif group && phrase_existence && @groups[group].delete_phrase(cmd[3..].join(" ")) - write - m = "Successfully deleted `#{cmd[3..].join(" ")}`. Phrases left in group `#{group}`: `#{@groups[group].phrases.join(", ")}`" - else - m = "Unable to delete phrase. Syntax: `word delete [group] [phrase]`" - end + if group && !@groups.has_key? group + return "Group does not exist" + elsif group && phrase_existence && @groups[group].delete_phrase(cmd[3..].join(" ")) + write + return "Successfully deleted `#{cmd[3..].join(" ")}`. Phrases left in group `#{group}`: `#{@groups[group].phrases.join(", ")}`" else - m = "Unknown or no Sub-Command given" + return "Unable to delete phrase. Syntax: `word delete [group] [phrase]`" end - when "whitelist", "wl" - case cmd[1]? - when "add" - # `whitelist add [group] [word]` - group = cmd[2]? - word = cmd[3]? + else + return "Unknown or no Sub-Command given" + end + when "whitelist", "wl" + case cmd[1]? + when "add" + # `whitelist add [group] [word]` + group = cmd[2]? + word = cmd[3]? - if group && !@groups.has_key? group - m = "Group does not exist" - elsif group && word && @groups[group].add_whitelist(word) - write - m = "Successfully added `#{word}` to whitelist of group `#{group}`." - else - m = "Unable to add whitelist. Syntax: `whitelist add [group] [word]`" - end - when "remove", "delete" - # `word delete [group] [word]` - group = cmd[2]? - word = cmd[3]? + if group && !@groups.has_key? group + return "Group does not exist" + elsif group && word && @groups[group].add_whitelist(word) + write + return "Successfully added `#{word}` to whitelist of group `#{group}`." + else + return "Unable to add whitelist. Syntax: `whitelist add [group] [word]`" + end + when "remove", "delete" + # `word delete [group] [word]` + group = cmd[2]? + word = cmd[3]? - if group && !@groups.has_key? group - m = "Group does not exist" - elsif group && word && @groups[group].delete_whitelist(word) - write - m = "Successfully removed whitelist `#{word}`" - else - m = "Unable to remove whitelist. Syntax: `whitelist delete [group] [word]`" - end + if group && !@groups.has_key? group + return "Group does not exist" + elsif group && word && @groups[group].delete_whitelist(word) + write + return "Successfully removed whitelist `#{word}`" else - m = "Unknown or no Sub-Command given" + return "Unable to remove whitelist. Syntax: `whitelist delete [group] [word]`" end - when "spam", "lookup" - phrase = cmd_string.lchop(cmd[0]).lstrip - findings = @message_cache.find(phrase).sort_by { |finding| -finding.score } - if findings.empty? - @client.create_message(msg.channel_id, "No results found!") - next + else + return "Unknown or no Sub-Command given" + end + when "spam", "lookup" + phrase = cmd_string.lchop(cmd[0]).lstrip + findings = @message_cache.find(phrase).sort_by { |finding| -finding.score } + if findings.empty? + return "No results found!" + end + user_ids = findings.map { |finding| finding.author_id }.uniq + messages = findings.map { |finding| sprintf("%5d - %20d: #{finding.content}", finding.score, finding.author_id) } + content = "Found #{findings.size} matches.\nUnique User-IDs encountered:\n#{user_ids.join(" ")}" + details = "Findings sorted by relevance:\n\nScore - UserId: Content\n#{messages.join("\n")}" + @client.upload_file(msg.channel_id, content, file: IO::Memory.new(details), filename: "details.txt") + return + when "list" + # including anything after `list` will trigger detailed view + details = true unless cmd[1]? + m = String.build do |str| + str.puts "```" + str.puts "Group [Mode]: Phrase(s) (Whitelist)" + str.puts + @groups.values.each do |group| + str << "#{group.name} [#{group.mode}]:" + tmp_str = "" + group.phrases.each { |x| tmp_str += " \"#{x}\"," } + str << tmp_str.rstrip(',') + str << (" (") + whitelist_str = "" + group.whitelist.each { |x| whitelist_str += " \"#{x}\"," } + str << whitelist_str.rstrip(',') + str << (" )") + str.puts end - user_ids = findings.map { |finding| finding.author_id }.uniq - messages = findings.map { |finding| sprintf("%5d - %20d: #{finding.content}", finding.score, finding.author_id) } - content = "Found #{findings.size} matches.\nUnique User-IDs encountered:\n#{user_ids.join(" ")}" - details = "Findings sorted by relevance:\n\nScore - UserId: Content\n#{messages.join("\n")}" - @client.upload_file(msg.channel_id, content, file: IO::Memory.new(details), filename: "details.txt") - when "list" - # including anything after `list` will trigger detailed view - details = true unless cmd[1]? - m = String.build do |str| + str.puts "```" + unless @dhash.empty? + str.puts "PFPs" str.puts "```" - str.puts "Group [Mode]: Phrase(s) (Whitelist)" + str.puts "Name#{details ? nil : ": dhash"}" str.puts - @groups.values.each do |group| - str << "#{group.name} [#{group.mode}]:" - tmp_str = "" - group.phrases.each { |x| tmp_str += " \"#{x}\"," } - str << tmp_str.rstrip(',') - str << (" (") - whitelist_str = "" - group.whitelist.each { |x| whitelist_str += " \"#{x}\"," } - str << whitelist_str.rstrip(',') - str << (" )") - str.puts + @dhash.each do |name, hash| + str.puts "#{name}#{details ? nil : ": #{hash}"}" end str.puts "```" - unless @dhash.empty? - str.puts "PFPs" - str.puts "```" - str.puts "Name#{details ? nil : ": dhash"}" - str.puts - @dhash.each do |name, hash| - str.puts "#{name}#{details ? nil : ": #{hash}"}" + end + end + return m + when "ban" + # `ban [from] [to] [type=creation]` + from_str = cmd[1]? + to_str = cmd[2]? + type_str = cmd[3]? + + if type_str + begin + type = BanType.parse(type_str) + rescue ex : ArgumentError + return "Invalid Time Type provided. Please try again. Valid values: #{BanType.values}" + end + else + type = BanType::Creation + end + + unless from_str && to_str + return "Please provide from and to values." + else + from = Time.unix(from_str.to_u64) + to = Time.unix(to_str.to_u64) + mentions = Array(UInt64).new + m = String.build do |str| + str.puts "Collecting users from #{from} to #{to}. Time-Span: #{(to - from).total_seconds}s" + + str.puts "```" + str << "!!massban" + @members_cache.each do |member| + date : Time + case type + when BanType::Creation + date = member.user.id.creation_time + when BanType::Join + date = member.joined_at.not_nil! + else + return "Error. Please try again" + end + + id = member.user.id.to_u64 + + if (date > from && date < to) + str << " #{id}" + mentions << id end - str.puts "```" + end + str.puts "```" + str.puts + mentions.each do |x| + str << " <@#{x}>" end end + return m end - - @client.create_message(msg.channel_id, m, allowed_mentions: NO_MENTION) if m + when "perm-ban" + # TODO end end diff --git a/src/handle_members.cr b/src/handle_members.cr index a1d4fcc..2cde742 100644 --- a/src/handle_members.cr +++ b/src/handle_members.cr @@ -21,6 +21,8 @@ class Slash end Log.info { "Processed whitelist chunking for #{chunk.guild_id} with #{chunk.members.size} members." } + @members_cache.concat(chunk.members) + t : Time::Span? = nil # Wait for turn @@ -64,6 +66,8 @@ class Slash @whitelist_cache.delete(id) + @members_cache.delete(@members_cache.find { |m| m.user.id == id }) + delayed_cache_removal(id) end diff --git a/src/slash.cr b/src/slash.cr index e8c9b20..1681c2c 100644 --- a/src/slash.cr +++ b/src/slash.cr @@ -51,6 +51,7 @@ class Slash intents = Discord::Gateway::Intents.flags(GuildMessages, GuildVoiceStates, GuildMembers) @client = Discord::Client.new(ENV["TOKEN"], intents: intents) @whitelist_cache = Set(Discord::Snowflake).new + @members_cache = Set(Discord::GuildMember).new @server = ENV["SERVER"].to_u64 @regex = /^<@!?#{@client.client_id}>\s*(?.*)/