Browse Source

Major Refactoring

master
parent
commit
27bc42acf9
No known key found for this signature in database GPG Key ID: DA34C790D267C164
25 changed files with 297 additions and 352 deletions
  1. +1
    -1
      shard.yml
  2. +0
    -7
      spec/Commands_spec.cr
  3. +2
    -2
      spec/spec_helper.cr
  4. +3
    -3
      src/Arguments.cr
  5. +22
    -57
      src/Bampersand.cr
  6. +6
    -0
      src/DiscordCrAddenda.cr
  7. +16
    -3
      src/Init.cr
  8. +10
    -12
      src/Perms.cr
  9. +5
    -4
      src/Util.cr
  10. +39
    -35
      src/commands/config.cr
  11. +17
    -19
      src/commands/core.cr
  12. +0
    -4
      src/commands/hulp.cr
  13. +4
    -2
      src/commands/kiosk.cr
  14. +25
    -23
      src/commands/mod.cr
  15. +3
    -1
      src/commands/ops.cr
  16. +15
    -17
      src/commands/tag.cr
  17. +2
    -1
      src/commands/util.cr
  18. +16
    -23
      src/modules/Board.cr
  19. +40
    -57
      src/modules/Commands.cr
  20. +20
    -24
      src/modules/Config.cr
  21. +6
    -6
      src/modules/JoinLeaveLog.cr
  22. +8
    -10
      src/modules/Killfile.cr
  23. +4
    -4
      src/modules/Mirroring.cr
  24. +22
    -24
      src/modules/ModTools.cr
  25. +11
    -13
      src/modules/RoleKiosk.cr

+ 1
- 1
shard.yml View File

@ -1,5 +1,5 @@
name: Bampersand
version: 0.17.1
version: 0.18.0
authors:
- deing <admin@15318.de>


+ 0
- 7
spec/Commands_spec.cr View File

@ -1,7 +0,0 @@
require "./spec_helper"
context Commands do
it "loaded commands" do
Commands.command_info.size > 0
end
end

+ 2
- 2
spec/spec_helper.cr View File

@ -1,5 +1,5 @@
require "spec"
require "logger"
LOG = Logger.new(STDOUT, level: Logger::DEBUG, progname: "B& SPEC")
require "discordcr"
require "../src/Bampersand"
LOG = Logger.new(STDOUT, level: Logger::DEBUG, progname: "B& SPEC")

+ 3
- 3
src/Arguments.cr View File

@ -27,7 +27,7 @@ module Arguments
def to_channel(input)
begin
input = input.delete("<#>").to_u64
channel = cache!.resolve_channel(input)
channel = CACHE.resolve_channel(input)
rescue e
LOG.error("to_channel failed to resolve #{input}")
raise "Invalid channel" if channel.nil?
@ -38,7 +38,7 @@ module Arguments
def to_user(input)
begin
input = input.delete("<@!>").to_u64
user = cache!.resolve_user(input)
user = CACHE.resolve_user(input)
rescue e
LOG.error("to_user failed to resolve #{input}")
raise "Invalid user" if user.nil?
@ -49,7 +49,7 @@ module Arguments
def to_role(input)
begin
input = input.delete("<@&>").to_u64
role = cache!.resolve_role(input)
role = CACHE.resolve_role(input)
rescue e
LOG.error("to_role failed to resolve #{input}")
raise "Invalid role" if role.nil?


+ 22
- 57
src/Bampersand.cr View File

@ -1,29 +1,14 @@
# This is the main code for initializing the bot client and
# This is the code necessary for connecting to Discord.
# The binary's entry point is in Init.cr.
require "db"
require "discordcr"
require "dotenv"
require "logger"
require "sqlite3"
# Fix for modify_guild_role_positions until it's merged into their master
require "./DiscordCr"
# This makes the client and its cache globally available. It's ugly
# but shorter than writing Bampersand.client.not_nil!.whatever
macro bot!
Bampersand.client.not_nil!
end
macro cache!
Bampersand.cache.not_nil!
end
require "./DiscordCrAddenda"
require "./Util"
require "./State"
require "./Arguments"
require "./Perms"
require "./modules/Config"
require "./modules/Mirroring"
require "./modules/Board"
require "./modules/JoinLeaveLog"
@ -32,78 +17,58 @@ require "./modules/Killfile"
require "./modules/RoleKiosk"
require "./modules/Commands"
module Bampersand
VERSION = `shards version`.chomp
PRESENCES = ["your concerns", "endless complaints", "socialist teachings", "the silence of the lambs", "anarchist teachings", "emo poetry", "FREUDE SCHÖNER GÖTTERFUNKEN", "the heat death of the universe", "[ASMR] Richard Stallman tells you to use free software", "the decline of western civilisation", "4'33'' (Nightcore Remix)", "General Protection Fault", "breadtube", "the book of origin"]
STARTUP = Time.monotonic
DATABASE = DB.open "sqlite3://./bampersand.sqlite3"
# Needs to be nilable as we don't want to connect to discord when running
# tests, so the client init is in a method which isn't guaranteed to run
@@bot : Discord::Client?
@@cache : Discord::Cache?
# Don't use these, but the bot! and cache! macros instead
def self.client
@@bot
end
def self.cache
@@cache
end
require "./commands/*"
# Entry method for booting the bot
def self.start
module Bampersand
def self.boot
# Prepare connection to Discord
client = Discord::Client.new(token: "Bot #{ENV["token"]}")
client.cache = Discord::Cache.new(client)
@@bot = client
@@cache = @@bot.not_nil!.cache.not_nil!
# Set up event handlers
bot!.on_message_create do |msg|
client.on_message_create do |msg|
ModTools.enforce_slowmode(msg)
Mirroring.handle_message(msg)
Commands.handle_message(msg) unless msg.author.bot
end
bot!.on_ready do
client.on_ready do
presences = ["your concerns", "endless complaints", "socialist teachings", "the silence of the lambs", "anarchist teachings", "emo poetry", "FREUDE SCHÖNER GÖTTERFUNKEN", "the heat death of the universe", "[ASMR] Richard Stallman tells you to use free software", "the decline of western civilisation", "4'33'' (Nightcore Remix)", "General Protection Fault", "breadtube", "the book of origin"]
if ENV["runas"] == "prod"
bot!.status_update(
client.status_update(
"online",
Discord::GamePlaying.new(name: PRESENCES.sample, type: 2i64)
Discord::GamePlaying.new(name: presences.sample, type: 2i64)
)
elsif ENV["runas"] == "dev"
bot!.status_update(
client.status_update(
"online",
Discord::GamePlaying.new(name: VERSION.to_s, type: 3i64)
)
else
raise "Invalid run-as environment #{ENV["runas"]}"
raise "Invalid environment #{ENV["runas"]}"
end
end
bot!.on_message_reaction_add do |payload|
client.on_message_reaction_add do |payload|
RoleKiosk.handle_reaction_add(payload)
Board.handle_reaction(payload)
Board.handle_reaction_add(payload)
end
bot!.on_message_reaction_remove do |payload|
client.on_message_reaction_remove do |payload|
RoleKiosk.handle_reaction_remove(payload)
end
bot!.on_guild_create do |payload|
client.on_guild_create do |payload|
LOG.info(
"Joined new guild #{payload.name} [#{payload.id}] — Owner is #{payload.owner_id}"
)
Killfile.handle_join(payload)
end
bot!.on_guild_member_add do |payload|
client.on_guild_member_add do |payload|
JoinLeaveLog.handle_join(payload)
end
bot!.on_guild_member_remove do |payload|
client.on_guild_member_remove do |payload|
JoinLeaveLog.handle_leave(payload)
end
LOG.info("Loaded Bampersand v#{Bampersand::VERSION}")
LOG.info("Loaded Bampersand v#{VERSION}")
LOG.info("WHAT ARE YOUR COMMANDS?")
# Then, by all means, let there be … life!
bot!.run
client
end
end

src/DiscordCr.cr → src/DiscordCrAddenda.cr View File

@ -6,4 +6,10 @@ module Discord
"#{self.username}##{self.discriminator}"
end
end
struct Snowflake
def to_i64
self.to_u64.to_i64
end
end
end

+ 16
- 3
src/Init.cr View File

@ -3,19 +3,32 @@
# which is supposed to close the DB, before booting the main bot code
# from the Bampersand.cr file.
require "db"
require "dotenv"
require "logger"
require "sqlite3"
LOG = Logger.new(STDOUT, level: Logger::DEBUG, progname: "B&")
LOG.info("Initializing…")
Dotenv.load!
require "./Bampersand"
VERSION = `shards version`.chomp
STARTUP = Time.monotonic
DATABASE = DB.open "sqlite3://./bampersand.sqlite3"
SHUTDOWN = ->(s : Signal) {
LOG.fatal "Received #{s}"
Bampersand::DATABASE.close
DATABASE.close
LOG.fatal "This program is halting now, checkmate Alan"
exit 0
}
Signal::INT.trap &SHUTDOWN
Signal::TERM.trap &SHUTDOWN
Bampersand.start
require "./Bampersand"
BOT = Bampersand.boot
CACHE = BOT.cache.not_nil!
# Then, by all means, let there be … life!
BOT.run

+ 10
- 12
src/Perms.cr View File

@ -3,11 +3,9 @@ module Perms
extend self
# Maps Guild-ID => (Level => Role-ID)
@@perms : Hash(UInt64, Hash(Level, UInt64?)) = load_perms()
def load_perms
@@perms : Hash(UInt64, Hash(Level, UInt64?)) = ->{
perms = {} of UInt64 => Hash(Level, UInt64?)
Bampersand::DATABASE.query "select * from perms" do |rs|
DATABASE.query "select * from perms" do |rs|
rs.each do
perms[rs.read(Int64).to_u64] = {
Level::Admin => rs.read(Int64?).try(&.to_u64),
@ -16,7 +14,7 @@ module Perms
end
end
perms
end
}.call
enum Level
User; Moderator; Admin; Owner; Operator
@ -32,19 +30,19 @@ module Perms
# Can't run privileged commands outside a guild
return Level::User if guild_id.nil?
guild_id = guild_id.not_nil!
if user_id == cache!.resolve_guild(guild_id).owner_id.to_u64
if user_id == CACHE.resolve_guild(guild_id).owner_id.to_u64
return Level::Owner
end
guild_perms = @@perms[guild_id]?
if guild_perms && guild_perms[Level::Admin]?
member = cache!.resolve_member(guild_id, user_id)
member = CACHE.resolve_member(guild_id, user_id)
role_id = guild_perms[Level::Admin]
return Level::Admin if member.roles.any? do |role|
role.to_u64 == role_id
end
end
if guild_perms && guild_perms[Level::Moderator]?
member = cache!.resolve_member(guild_id, user_id)
member = CACHE.resolve_member(guild_id, user_id)
role_id = guild_perms[Level::Moderator]
return Level::Moderator if member.roles.any? do |role|
role.to_u64 == role_id
@ -54,9 +52,9 @@ module Perms
end
def update_perms(guild_id, level, role_id)
@@perms[guild_id] = {} of Level => UInt64? unless @@perms[guild_id]?
@@perms[guild_id][level] = role_id
Bampersand::DATABASE.exec(
@@perms[guild_id.to_u64] = {} of Level => UInt64? unless @@perms[guild_id]?
@@perms[guild_id.to_u64][level] = role_id
DATABASE.exec(
"insert into perms values (?,?,?)", guild_id.to_i64,
@@perms[guild_id][Level::Admin]?.try(&.to_i64),
@@perms[guild_id][Level::Moderator]?.try(&.to_i64)
@ -67,7 +65,7 @@ module Perms
# already integrated into Command
macro assert_level(level)
unless Perms.check(
ctx.guild_id, ctx.issuer.id.to_u64, Perms::Level::{{level}}
ctx.message.guild_id, ctx.issuer.id.to_u64, Perms::Level::{{level}}
)
raise "Unauthorized. Required: {{level}}"
end


+ 5
- 4
src/Util.cr View File

@ -4,7 +4,7 @@ module Util
# Get (nilable) guild id from a channel id
def guild(client, channel_id)
channel = cache!.resolve_channel(channel_id)
channel = CACHE.resolve_channel(channel_id)
channel.guild_id
end
@ -14,9 +14,9 @@ module Util
return true if user_id == ENV["admin"].to_u64
return true if context.guild_id.nil?
guild_id = context.guild_id.not_nil!
member = cache!.resolve_member(guild_id, user_id)
member = CACHE.resolve_member(guild_id, user_id)
roles = member.roles.map do |element|
cache!.resolve_role(element)
CACHE.resolve_role(element)
end
roles.any? do |element|
element.permissions.includes?(permissions) ||
@ -32,7 +32,8 @@ module Util
# Currently not needed as all guild-requiring commands are level-privileged
def assert_guild(context)
raise "This command can only be used in guilds" if context.guild_id.nil?
raise "This command can only be used in guilds" if context.message.guild_id.nil?
context.message.guild_id.not_nil!
end
# Helper for the Board system, stringifies custom and unicode emoji


+ 39
- 35
src/commands/config.cr View File

@ -1,94 +1,98 @@
macro common(feature_enum)
require "../modules/Commands"
macro macro_stop_feature(feature_enum)
Arguments.assert_count(args, 1)
guild = ctx.guild_id.not_nil!
guild = ctx.message.guild_id.not_nil!
if args[0] == "stop"
State.feature(guild, State::Features::{{feature_enum}}, false)
Config.feature(guild, Config::Features::{{feature_enum}}, false)
next true
end
end
Commands.register_command("config mirror", "Sets up the Mirroring feature.", Perms::Level::Operator) do |args, ctx|
common(Mirror)
guild = ctx.guild_id.not_nil!
macro_stop_feature(Mirror)
guild_id = ctx.message.guild_id.not_nil!
channel = Arguments.at_position(args, 0, :channel)
raise "Bad mirror target." if channel.id == ctx.channel_id
State.set(guild, {mirror_in: ctx.channel_id, mirror_out: channel.id.to_u64})
State.feature(guild, State::Features::Mirror, true)
raise "Bad mirror target." if channel.id == ctx.message.channel_id
Config.set(guild_id, {mirror_in: ctx.message.channel_id.to_u64, mirror_out: channel.id.to_u64})
Config.feature(guild_id, Config::Features::Mirror, true)
true
end
Commands.register_command("config board", "Sets up the Board Feature.", Perms::Level::Admin) do |args, ctx|
common(Board)
guild = ctx.guild_id.not_nil!
macro_stop_feature(Board)
Arguments.assert_count(args, 3)
emoji = args[0]
channel = Arguments.at_position(args, 1, :channel)
guild_id = Util.assert_guild(ctx)
min_reacts = args[2].to_u32
raise "min_reacts must be greater than zero." if min_reacts == 0
State.set(
guild,
Config.set(
guild_id,
{
board_emoji: emoji,
board_channel: channel.id.to_u64,
board_emoji: args[0],
board_channel: Arguments.at_position(args, 1, :channel).id.to_u64,
board_min_reacts: min_reacts,
}
)
State.feature(guild, State::Features::Board, true)
Config.feature(guild_id, Config::Features::Board, true)
true
end
Commands.register_command("config join-log", "Sets up the JoinLog feature.", Perms::Level::Admin) do |args, ctx|
common(JoinLog)
guild = ctx.guild_id.not_nil!
macro_stop_feature(JoinLog)
guild_id = Util.assert_guild(ctx)
Arguments.assert_count(args, 2)
channel = Arguments.at_position(args, 0, :channel)
args.shift
text = args.join(" ").strip
State.feature(guild, State::Features::JoinLog, true)
State.set(guild, {join_channel: channel.id.to_u64, join_text: text})
Config.set(guild_id, {join_channel: channel.id.to_u64, join_text: text})
Config.feature(guild_id, Config::Features::JoinLog, true)
true
end
Commands.register_command("config leave-log", "Sets up the LeaveLog Feature.", Perms::Level::Admin) do |args, ctx|
common(LeaveLog)
guild = ctx.guild_id.not_nil!
macro_stop_feature(LeaveLog)
guild_id = Util.assert_guild(ctx)
Arguments.assert_count(args, 2)
channel = Arguments.at_position(args, 0, :channel)
args.shift
text = args.join(" ").strip
State.feature(guild, State::Features::LeaveLog, true)
State.set(guild, {leave_channel: channel.id.to_u64, leave_text: text})
Config.feature(guild_id, Config::Features::LeaveLog, true)
Config.set(guild_id, {leave_channel: channel.id.to_u64, leave_text: text})
true
end
Commands.register_command("config slowmode", "Enforces slowmode in the current channel for everyone.", Perms::Level::Admin) do |args, ctx|
guild = ctx.guild_id.not_nil!
guild_id = Util.assert_guild(ctx)
Arguments.assert_count(args, 1)
if args[0].downcase == "stop"
ModTools.remove_channel_slowmode(ctx.channel_id)
next true
ModTools.remove_channel_slowmode(ctx.message.channel_id)
else
secs = args[0].to_u32
ModTools.set_channel_slowmode(ctx.message.channel_id, secs)
end
secs = args[0].to_u32
ModTools.set_channel_slowmode(ctx.channel_id, secs)
true
end
Commands.register_command("config mod-role", "Sets the Moderator Level role.", Perms::Level::Admin) do |args, ctx|
guild = ctx.guild_id.not_nil!
guild_id = Util.assert_guild(ctx)
role = Arguments.at_position(args, 0, :role)
Perms.update_perms(guild, Perms::Level::Moderator, role.id.to_u64)
Perms.update_perms(guild_id, Perms::Level::Moderator, role.id.to_u64)
true
end
Commands.register_command("config admin-role", "Sets the Admin Level role.", Perms::Level::Owner) do |args, ctx|
guild = ctx.guild_id.not_nil!
guild_id = Util.assert_guild(ctx)
role = Arguments.at_position(args, 0, :role)
Perms.update_perms(guild, Perms::Level::Admin, role.id.to_u64)
Perms.update_perms(guild_id, Perms::Level::Admin, role.id.to_u64)
true
end
Commands.register_command("config print", "Stringifies the configuration.", Perms::Level::Admin) do |_args, ctx|
"```#{State.get(ctx.guild_id)}```"
"```#{Config.get(ctx.message.guild_id)}```"
end
Commands.register_command("config", "[Edit per-guild configuration]", Perms::Level::Admin) do


+ 17
- 19
src/commands/core.cr View File

@ -1,24 +1,22 @@
PINGS = ["Pyongyang!", "Ding!", "Pong!", "[reverberating PONG]", "Plonk."]
require "../modules/Commands"
PINGS = ["Pyongyang!", "Ding!", "Pong!", "[reverberating PONG]", "Plonk."]
Commands.register_command("ping", "Pongs you.", Perms::Level::User) do |_args, ctx|
ping = Time.utc_now - ctx.timestamp
ping = Time.utc_now - ctx.message.timestamp
":ping_pong: " + PINGS.sample + " | `#{ping.total_milliseconds.to_i}ms`"
end
Commands.register_command("help", "Lists commands. Pass a single argument to search command descriptions.", Perms::Level::User) do |args|
output = if args.size != 1
Commands.command_info.keys.select do |e|
!e.includes?(" ")
end.reduce(" ") do |memo, e|
memo + "*`#{e}`*, "
end.rchop(", ")
else
Commands.command_info.keys.select do |e|
e.includes?(args[0])
end.sort!.reduce(" ") do |memo, e|
memo + "*`#{e}`* #{Commands.command_info[e].desc}\n"
end
end
Commands.register_command("help", "Lists commands. Pass a single argument to search for commands.", Perms::Level::User) do |args|
output = ""
if args.empty?
output = Commands.registry.keys.reject(&.includes?(" ")).reduce(" ") do |memo, e|
memo + "*`#{e}`*, "
end.rchop(", ")
else
output = Commands.registry.keys.select(&.includes?(args[0])).sort!.reduce(" ") do |memo, e|
memo + "*`#{e}`* #{Commands.registry[e].description}\n"
end
end
{
text: output.lstrip.size > 0 ? output : "No results found.",
title: "Commands",
@ -26,9 +24,9 @@ Commands.register_command("help", "Lists commands. Pass a single argument to sea
end
Commands.register_command("about", "Displays stats about Bampersand and links to further resources.", Perms::Level::User) do
uptime = Time.monotonic - Bampersand::STARTUP
uptime = Time.monotonic - STARTUP
{
title: "**BAMPERSAND VERSION #{Bampersand::VERSION}**",
text: "This is a simple utility bot for Discord powered by [Crystal](https://crystal-lang.org).\nYou can take a peek <:blobpeek:559732380697362482> at the [documentation](https://git.15318.de/Dingens/Bampersand/wiki/Home) and the [source code](https://git.15318.de/Dingens/Bampersand)!\nCurrently running on #{cache!.guilds.size} guilds, serving #{cache!.users.size} users.\nUptime is #{uptime.days}d #{uptime.hours}h #{uptime.minutes}m #{uptime.seconds}s. Bot operator is <@#{ENV["admin"]}>.",
title: "**BAMPERSAND VERSION #{VERSION}**",
text: "This is a simple utility bot for Discord powered by [Crystal](https://crystal-lang.org).\nYou can take a peek <:blobpeek:559732380697362482> at the [documentation](https://git.15318.de/Dingens/Bampersand/wiki/Home) and the [source code](https://git.15318.de/Dingens/Bampersand)!\nCurrently running on #{CACHE.guilds.size} guilds, serving #{CACHE.users.size} users.\nUptime is #{uptime.days}d #{uptime.hours}h #{uptime.minutes}m #{uptime.seconds}s. Bot operator is <@#{ENV["admin"]}>.",
}
end

+ 0
- 4
src/commands/hulp.cr View File

@ -1,4 +0,0 @@
Commands.register_command("hulp", "[DATA MISSING]", Perms::Level::User) do |_args, ctx|
next "Ffs\nDon't do that again <@#{ctx.issuer.id}>. Look at my flair\nI only need 0.001% of my power to wipe you out" unless Perms.check(ctx.guild_id, ctx.issuer.id, Perms::Level::Operator)
true
end

+ 4
- 2
src/commands/kiosk.cr View File

@ -1,3 +1,5 @@
require "../modules/Commands"
Commands.register_command("rolekiosk update", "Configure a message as a role kiosk.", Perms::Level::Admin) do |args|
Arguments.assert_count(args, 2)
location = args[0].split(":")
@ -8,13 +10,13 @@ Commands.register_command("rolekiosk update", "Configure a message as a role kio
old_emoji = RoleKiosk.kiosk(message_id)
if old_emoji
old_emoji.keys.each do |emoji|
bot!.delete_own_reaction(channel_id, message_id, emoji.delete("<>"))
BOT.delete_own_reaction(channel_id, message_id, emoji.delete("<>"))
end
end
# Add reaction template
emojis = args[1].split(";").map { |x| x.split("|")[0] }
emojis.each do |emoji|
bot!.create_reaction(channel_id, message_id, emoji.delete("<>"))
BOT.create_reaction(channel_id, message_id, emoji.delete("<>"))
end
RoleKiosk.update_kiosk(message_id, args[1])
true


+ 25
- 23
src/commands/mod.cr View File

@ -1,11 +1,13 @@
require "../modules/Commands"
Commands.register_command("ban", "Attempts to ban all mentioned users.", Perms::Level::Admin) do |args, ctx|
Arguments.assert_count(args, 1)
guild_id = Util.assert_guild(ctx)
output = "Attempting to ban #{args.size} members…\nResponsible Moderator: <@#{ctx.issuer.id}>\n"
guild_id = ctx.guild_id.as(UInt64)
args.each do |argument|
begin
user = Arguments.to_user(argument)
bot!.create_guild_ban(
BOT.create_guild_ban(
guild_id, user.id, nil,
"Banned by Bampersand on behalf of #{ctx.issuer.tag} (#{ctx.issuer.id}) at #{Time.utc_now}."
)
@ -19,12 +21,12 @@ end
Commands.register_command("kick", "Attempts to kick all mentioned users.", Perms::Level::Moderator) do |args, ctx|
Arguments.assert_count(args, 1)
guild_id = Util.assert_guild(ctx)
output = "Attempting to kick #{args.size} members…\nResponsible Moderator: <@#{ctx.issuer.id}>\n"
guild_id = ctx.guild_id.as(UInt64)
args.each do |argument|
begin
user = Arguments.to_user(argument)
bot!.remove_guild_member(guild_id, user.id)
BOT.remove_guild_member(guild_id, user.id)
output += ":heavy_check_mark: Kicked <@#{user.id}>" + "\n"
rescue
output += ":x: Failed to kick #{argument}\n"
@ -35,12 +37,12 @@ end
Commands.register_command("unban", "Attempts to unban all mentioned users.", Perms::Level::Admin) do |args, ctx|
Arguments.assert_count(args, 1)
guild_id = Util.assert_guild(ctx)
output = "Attempting to unban #{args.size} members…\nResponsible Moderator: <@#{ctx.issuer.id}>\n"
guild_id = ctx.guild_id.as(UInt64)
args.each do |argument|
begin
user = Arguments.to_user(argument)
bot!.remove_guild_ban(guild_id, user.id)
BOT.remove_guild_ban(guild_id, user.id)
output += ":heavy_check_mark: Pardoned <@#{user.id}>" + "\n"
rescue
output += ":x: Failed to unban #{argument}\n"
@ -51,14 +53,14 @@ end
Commands.register_command("mute", "Attempts to mute all mentioned users.", Perms::Level::Moderator) do |args, ctx|
Arguments.assert_count(args, 1)
mute_role = ModTools.mute_role?(ctx.guild_id.not_nil!)
mute_role = ModTools.create_mute_role(ctx.guild_id.not_nil!) if mute_role.nil?
guild_id = Util.assert_guild(ctx)
mute_role = ModTools.mute_role?(ctx.message.guild_id.not_nil!)
mute_role = ModTools.create_mute_role(ctx.message.guild_id.not_nil!) if mute_role.nil?
output = "Attempting to mute #{args.size} members…\nResponsible Moderator: <@#{ctx.issuer.id}>\n"
guild_id = ctx.guild_id.as(UInt64)
args.each do |argument|
begin
user = Arguments.to_user(argument)
bot!.add_guild_member_role(guild_id, user.id.to_u64, mute_role.id.to_u64)
BOT.add_guild_member_role(guild_id.to_u64, user.id.to_u64, mute_role.id.to_u64)
output += ":heavy_check_mark: Muted <@#{user.id}>" + "\n"
rescue
output += ":x: Failed to mute #{argument}\n"
@ -69,15 +71,15 @@ end
Commands.register_command("unmute", "Attempts to unmute all mentioned users.", Perms::Level::Moderator) do |args, ctx|
Arguments.assert_count(args, 1)
mute_role = ModTools.mute_role?(ctx.guild_id.not_nil!)
guild_id = Util.assert_guild(ctx)
mute_role = ModTools.mute_role?(ctx.message.guild_id.not_nil!)
raise "Mute role not found." if mute_role.nil?
output = "Attempting to unmute #{args.size} members…\nResponsible Moderator: <@#{ctx.issuer.id}>\n"
guild_id = ctx.guild_id.as(UInt64)
args.each do |argument|
begin
user = Arguments.to_user(argument)
bot!.remove_guild_member_role(
guild_id, user.id.to_u64, mute_role.id.to_u64
BOT.remove_guild_member_role(
guild_id.to_u64, user.id.to_u64, mute_role.id.to_u64
)
output += ":heavy_check_mark: Unmuted <@#{user.id}>" + "\n"
rescue
@ -90,10 +92,10 @@ end
Commands.register_command("warn add", "Adds a warning for the mentioned user, reason optional.", Perms::Level::Moderator) do |args, ctx|
target_user = Arguments.at_position(args, 0, :user).as(Discord::User)
reason = args.size > 0 ? args[1..].join(" ") : ""
Bampersand::DATABASE.exec(
DATABASE.exec(
"insert into warnings (guild_id, user_id, mod_id, text) values (?,?,?,?)",
ctx.guild_id.not_nil!.to_i64, target_user.id.to_u64.to_i64,
ctx.issuer.id.to_u64.to_i64, reason)
ctx.message.guild_id.not_nil!.to_i64, target_user.id.to_i64,
ctx.issuer.id.to_i64, reason)
{
title: "Warning added for #{target_user.tag}",
text: "Responsible Moderator<@#{ctx.issuer.id}>\n#{reason}",
@ -103,9 +105,9 @@ Commands.register_command("warn list", "Lists all warnings for the mentioned use
target_user = Arguments.at_position(args, 0, :user).as(Discord::User)
output = ""
count = 0
Bampersand::DATABASE.query(
DATABASE.query(
"select mod_id, text, timestamp from warnings where guild_id == ? and user_id == ?",
ctx.guild_id.not_nil!.to_i64, target_user.id.to_u64.to_i64
ctx.message.guild_id.not_nil!.to_i64, target_user.id.to_i64
) do |rs|
rs.each do
mod_id = rs.read(Int64)
@ -119,17 +121,17 @@ Commands.register_command("warn list", "Lists all warnings for the mentioned use
end
Commands.register_command("warn remove", "Removes the oldest warning for the mentioned user.", Perms::Level::Moderator) do |args, ctx|
target_user = Arguments.at_position(args, 0, :user).as(Discord::User)
Bampersand::DATABASE.exec(
DATABASE.exec(
"delete from warnings where guild_id == ? and user_id == ? limit 1",
ctx.guild_id.not_nil!.to_i64, target_user.id.to_u64.to_i64
ctx.message.guild_id.not_nil!.to_i64, target_user.id.to_i64
)
true
end
Commands.register_command("warn expunge", "Removes all warnings for the mentioned user.", Perms::Level::Admin) do |args, ctx|
target_user = Arguments.at_position(args, 0, :user).as(Discord::User)
Bampersand::DATABASE.exec(
DATABASE.exec(
"delete from warnings where guild_id == ? and user_id == ?",
ctx.guild_id.not_nil!.to_i64, target_user.id.to_u64.to_i64
ctx.message.guild_id.not_nil!.to_i64, target_user.id.to_i64
)
true
end


+ 3
- 1
src/commands/ops.cr View File

@ -1,3 +1,5 @@
require "../modules/Commands"
Commands.register_command("ops restart", "Restarts Bampersand.", Perms::Level::Operator) do
system("sudo systemctl restart bampersand")
raise "You should not be able to see this."
@ -5,7 +7,7 @@ end
Commands.register_command("ops rebuild", "Rebuilds Bampersand from the latest source.", Perms::Level::Operator) do |_args, ctx|
raise "Pull failed" unless system("git pull origin master")
raise "Build failed" unless system("shards build --release")
"Successfully rebuilt in #{Time.utc_now - ctx.timestamp}."
"Successfully rebuilt in #{Time.utc_now - ctx.message.timestamp}."
end
Commands.register_command("ops blacklist", "Adds a guild to the killfile and leaves it.", Perms::Level::Operator) do |args|
Arguments.assert_count(args, 1)


+ 15
- 17
src/commands/tag.cr View File

@ -1,33 +1,32 @@
require "../modules/Commands"
Commands.register_command("tag update", "Updates/creates the tag with the passed name to the passed content.", Perms::Level::Moderator) do |args, ctx|
Perms.assert_level(Moderator)
guild_id = Util.assert_guild(ctx)
Arguments.assert_count(args, 2)
guild = ctx.guild_id.not_nil!
tag_name = args.shift
raise "Tag name may not contain newlines." if tag_name.includes?("\n")
tag_content = args.join(" ")
Bampersand::DATABASE.exec(
DATABASE.exec(
"insert into tags (guild_id, name, content) values (?,?,?)",
guild.to_i64, tag_name, tag_content
guild_id.to_i64, tag_name, tag_content
)
true
end
Commands.register_command("tag delete", "Deletes the tag with the passed name.", Perms::Level::Moderator) do |args, ctx|
Perms.assert_level(Moderator)
guild_id = Util.assert_guild(ctx)
Arguments.assert_count(args, 1)
guild = ctx.guild_id.not_nil!
tag_name = args.shift
Bampersand::DATABASE.exec(
DATABASE.exec(
"delete from tags where guild_id == ? and name == ?",
guild.to_i64, tag_name
guild_id.to_i64, tag_name
)
true
end
Commands.register_command("tag list", "Lists all defined tags.", Perms::Level::User) do |_args, ctx|
Util.assert_guild(ctx)
guild_id = Util.assert_guild(ctx)
output = ""
guild = ctx.guild_id.not_nil!
Bampersand::DATABASE.query(
"select name from tags where guild_id == ?", guild.to_i64
DATABASE.query(
"select name from tags where guild_id == ?", guild_id.to_i64
) do |rs|
rs.each do
output += " `#{rs.read(String)}`"
@ -45,18 +44,17 @@ TAG_HELP = {
}
Commands.register_command("tag", "[Edit and display custom messages]", Perms::Level::User) do |args, ctx|
next TAG_HELP if args.size == 0
Util.assert_guild(ctx)
guild = ctx.guild_id.not_nil!
guild_id = Util.assert_guild(ctx)
tag_name = args.shift
output = ""
Bampersand::DATABASE.query(
DATABASE.query(
"select content from tags where guild_id == ? and name == ?",
guild.to_i64, tag_name
guild_id.to_i64, tag_name
) do |rs|
rs.each do
output = rs.read(String)
end
end
raise "404 Tag Not Found" if output.size == 0
raise "No such tag" if output.size == 0
{title: "**#{tag_name.upcase}**", text: output}
end

+ 2
- 1
src/commands/util.cr View File

@ -1,3 +1,4 @@
require "../modules/Commands"
require "http/client"
Commands.register_command("leo", "Shortens the passed URL using leo.immobilien.", Perms::Level::User) do |args|
@ -19,6 +20,6 @@ Commands.register_command("info", "Displays debug information about you.", Perms
Your ID: `#{ctx.issuer.id}`
Your Permissions: `#{ctx.permissions.value}`
Your Level: `#{ctx.level}`
Message Timestamp: `#{ctx.timestamp}`
Message Timestamp: `#{ctx.message.timestamp}`
OUT
end

+ 16
- 23
src/modules/Board.cr View File

@ -2,35 +2,28 @@ module Board
# This module handles the reaction based best-of tracker.
extend self
# Maps Message-ID => Message-ID
@@board_messages : Hash(UInt64, UInt64) = load_board
def load_board
# Maps source Message ID => Board Message ID
@@board_messages : Hash(UInt64, UInt64) = ->{
board_data = {} of UInt64 => UInt64
Bampersand::DATABASE.query(
DATABASE.query(
"select source_message, board_message from board"
) do |rs|
raise "Invalid column count" unless rs.column_count == 2
rs.each do
board_data[rs.read(Int64).to_u64] = rs.read(Int64).to_u64
end
end
LOG.info("Loaded Board Module: #{board_data.size} stored board messages")
board_data
end
}.call
# The event handler calls this.
def handle_reaction(payload)
guild = Util.guild(bot!, payload.channel_id)
return unless guild
# Abort if a) board is disabled
return unless State.feature? guild, State::Features::Board
# b) Message is from the board channel
# c) The reaction isn't the correct emoji
config = State.get(guild)
return if payload.channel_id.to_u64 == config[:board_channel]
return unless Util.reaction_to_s(payload.emoji) == config[:board_emoji] || config[:board_emoji] == "*"
message = bot!.get_channel_message(payload.channel_id, payload.message_id)
def handle_reaction_add(payload)
guild = Util.guild(BOT, payload.channel_id)
return unless guild && Config.feature? guild, Config::Features::Board
guild_config = Config.get(guild)
return if payload.channel_id.to_u64 == guild_config[:board_channel]
return unless Util.reaction_to_s(payload.emoji) == guild_config[:board_emoji] || guild_config[:board_emoji] == "*"
message = BOT.get_channel_message(payload.channel_id, payload.message_id)
# Get the "target" reaction:
target_emoji = if config[:board_emoji] == "*"
# If we don't have a target emoji, take the one with the highest count
@ -50,7 +43,7 @@ module Board
if @@board_messages.has_key? payload.message_id
begin
bot!.edit_message(
BOT.edit_message(
config[:board_channel],
@@board_messages[payload.message_id.to_u64],
"",
@ -61,16 +54,16 @@ module Board
end
else
begin
posted_message = bot!.create_message(
posted_message = BOT.create_message(
config[:board_channel],
"",
build_embed(guild, message, count, emoji_s)
)
@@board_messages[payload.message_id.to_u64] = posted_message.id.to_u64
Bampersand::DATABASE.exec(
DATABASE.exec(
"insert into board (source_message, board_message) values (?,?)",
payload.message_id.to_u64.to_i64,
posted_message.id.to_u64.to_i64
payload.message_id.to_i64,
posted_message.id.to_i64
)
rescue e
LOG.error("Failed to post board message: #{e}")


+ 40
- 57
src/modules/Commands.cr View File

@ -1,3 +1,5 @@
require "../Perms"
require "../commands/*"
module Commands
@ -8,71 +10,36 @@ module Commands
# In contrast to just passing on the message struct, I can add arbitrary
# fields here.
record CommandContext,
message : Discord::Message,
issuer : Discord::User,
channel_id : UInt64,
guild_id : UInt64?,
timestamp : Time,
permissions : Discord::Permissions,
level : Perms::Level
record GuildOnlyContext, guild_id : UInt64?
# Command Metadata
record CommandInfo, desc : String, level : Perms::Level
record Command, description : String, level : Perms::Level, exec : CommandExecType
# The type of command executes
alias CommandType = Proc(Array(String), CommandContext, CommandResult)
alias CommandExecType = Proc(Array(String), CommandContext, CommandResult)
# NT renders to an embed, String to plain text response, bool to ✔ reaction
alias CommandResult = NamedTuple(title: String, text: String) | String | Bool
@@command_exec = {} of String => CommandType
@@command_info = {} of String => CommandInfo
@@registry = {} of String => Command
# This is the function all command definitions use to add their data/exec to
# the module's registry.
def register_command(
name, desc, perms, &execute : Array(String), CommandContext -> CommandResult
)
@@command_exec[name] = execute
@@command_info[name] = CommandInfo.new(desc, perms)
end
# Getters
def command_info
@@command_info
@@registry[name] = Command.new(desc, perms, execute)
end
def command_execs
@@command_execs
end
# Creates a CommandContext from a message object
def build_context(msg : Discord::Message)
guild = msg.guild_id
perms = if guild
member = cache!.resolve_member(guild, msg.author.id)
perms_tmp = Discord::Permissions::None
member.roles.each do |role_id|
role = cache!.resolve_role(role_id)
perms_tmp += role.permissions.value
end
perms_tmp
else
Discord::Permissions::None
end
CommandContext.new(
issuer: msg.author,
channel_id: msg.channel_id.to_u64,
guild_id: guild.try &.to_u64,
timestamp: msg.timestamp,
permissions: perms,
level: Perms.get_highest(guild, msg.author.id)
)
def registry
@@registry
end
# The event handler calls this.
# On match, execution continues in #run_command.
def handle_message(msg)
return unless msg.content.starts_with?(ENV["prefix"])
content = msg.content.lchop(ENV["prefix"])
@@command_exec.keys.each do |key|
@@registry.keys.each do |key|
next unless content.starts_with? key
arguments = content.lchop(key).split(" ")
arguments.delete("")
@ -84,21 +51,16 @@ module Commands
# Attempts to execute a command. #send_result handles rendering the output.
def run_command(msg, command, args)
# Privilege level checking
unless Perms.check(
msg.guild_id, msg.author.id, @@command_info[command].level
)
fail_str = "Unauthorized. Required: #{@@command_info[command].level}"
unless Perms.check(msg.guild_id, msg.author.id, @@registry[command].level)
LOG.warn(
"Refused to execute #{command} #{args} for #{msg.author.tag}: #{Perms.get_highest(msg.guild_id, msg.author.id)} < #{@@command_info[command].level}"
"Refused to execute #{command} #{args} for #{msg.author.tag}: #{Perms.get_highest(msg.guild_id, msg.author.id)} < #{@@registry[command].level}"
)
send_result(msg.channel_id, msg.id, command, :error, fail_str)
send_result(msg.channel_id, msg.id, command, :error, "Unauthorized. Required: #{@@registry[command].level}")
return
end
begin
LOG.info("#{msg.author.tag} issued #{command} #{args}")
output = @@command_exec[command].call(
args, build_context(msg)
)
output = @@registry[command].exec.call(args, build_context(msg))
send_result(msg.channel_id, msg.id, command, :success, output)
rescue e
send_result(msg.channel_id, msg.id, command, :error, e)
@ -106,23 +68,44 @@ module Commands
end
end
# Creates a CommandContext from a message object
def build_context(msg : Discord::Message)
guild = msg.guild_id
perms = Discord::Permissions::None
if guild
member = CACHE.resolve_member(guild, msg.author.id)
perms_tmp = Discord::Permissions::None
member.roles.each do |role_id|
role = CACHE.resolve_role(role_id)
perms_tmp += role.permissions.value
end
perms = perms_tmp
end
CommandContext.new(
message: msg,
issuer: msg.author,
permissions: perms,
level: Perms.get_highest(guild, msg.author.id)
)
end
# Renders the command output to discord.
def send_result(channel_id, message_id, command, result, output)
if result == :success
# Strings render to plain-text messages,
if output.is_a?(String)
bot!.create_message(channel_id, output)
BOT.create_message(channel_id, output)
# NamedTuples to embeds,
elsif output.is_a?(NamedTuple(title: String, text: String))
bot!.create_message(channel_id, "", embed: Discord::Embed.new(
BOT.create_message(channel_id, "", embed: Discord::Embed.new(
colour: 0x16161d, description: output[:text], title: output[:title]
))
# And `true` to a ✔ reaction.
elsif output.is_a?(Bool) && output
bot!.create_reaction(channel_id, message_id, "")
BOT.create_reaction(channel_id, message_id, "")
end
elsif result == :error
bot!.create_message(channel_id, "", embed: Discord::Embed.new(
BOT.create_message(channel_id, "", embed: Discord::Embed.new(
title: "**failed to execute: #{command}**".upcase,
colour: 0xdd2e44,
description: "`#{output.to_s}`"
@ -133,6 +116,6 @@ module Commands
end
LOG.info(
"Loaded #{Commands.command_info.size} commands: #{Commands.command_info.keys}"
"Loaded #{Commands.registry.size} commands."
)
end

src/State.cr → src/modules/Config.cr View File

@ -1,9 +1,8 @@
module State
module Config
# This module stores and manages guild-specific configuration.
extend self
# TODO: Turn this into a record/struct
alias GuildState = NamedTuple(
alias GuildConfig = NamedTuple(
features: Features,
mirror_in: UInt64,
mirror_out: UInt64,
@ -15,8 +14,16 @@ module State
leave_channel: UInt64,
leave_text: String)
@[Flags]
enum Features
Mirror
Board
JoinLog
LeaveLog
end
# All features are disabled and values set to null-like values (not nil!)
def default_state : GuildState
def default_state : GuildConfig
{
features: Features::None,
mirror_in: 0u64,
@ -31,12 +38,10 @@ module State
}
end
# Maps Guild-ID => State NT
@@state : Hash(UInt64, GuildState) = load_state()
def load_state
state = {} of UInt64 => GuildState
Bampersand::DATABASE.query "select * from state" do |rs|
# Maps Guild ID => Config NT
@@state : Hash(UInt64, GuildConfig) = ->{
state = {} of UInt64 => GuildConfig
DATABASE.query "select * from state" do |rs|
rs.each do
state[rs.read(Int64).to_u64] = {
features: Features.new(rs.read(Int32)),
@ -52,9 +57,9 @@ module State
}
end
end
LOG.info("Loaded State Module: #{state.keys.size} stored states")
LOG.info("Loaded Config Module: #{state.keys.size} stored states")
state
end
}.call
# Getter defaulting to the default state
def get(guild_id)
@ -62,12 +67,11 @@ module State
@@state[guild_id]
end
# Setter, writes to memory and DB immediately. Don't manipulate the features
# enum with this! Use State#feature instead.
# Don't manipulate the features enum with this, use Config#feature instead.
def set(guild_id, update)
new_state = get(guild_id).merge(update)
@@state[guild_id] = new_state
Bampersand::DATABASE.exec(
@@state[guild_id.to_u64] = new_state
DATABASE.exec(
"insert into state (guild_id, features, mirror_in, mirror_out, board_emoji, board_channel, board_min_reacts, join_channel, join_text, leave_channel, leave_text) values (?,?,?,?,?,?,?,?,?,?,?)",
guild_id.to_i64,
new_state[:features].to_i64,
@ -100,12 +104,4 @@ module State
def feature?(guild_id, feature)
get(guild_id)[:features].includes? feature
end
@[Flags]
enum Features
Mirror
Board
JoinLog
LeaveLog
end
end

+ 6
- 6
src/modules/JoinLeaveLog.cr View File

@ -3,17 +3,17 @@ module JoinLeaveLog
extend self
def handle_join(payload)
return unless State.feature?(payload.guild_id, State::Features::JoinLog)
config = State.get(payload.guild_id)
return unless Config.feature?(payload.guild_id, Config::Features::JoinLog)
config = Config.get(payload.guild_id)
out_string = config[:join_text].gsub("@user", "<@#{payload.user.id}>")
bot!.create_message(config[:join_channel], out_string)
BOT.create_message(config[:join_channel], out_string)
end
def handle_leave(payload)
return unless State.feature?(payload.guild_id, State::Features::LeaveLog)
config = State.get(payload.guild_id)
return unless Config.feature?(payload.guild_id, Config::Features::LeaveLog)
config = Config.get(payload.guild_id)
out_string = config[:leave_text].gsub("@user", "#{payload.user.tag} (`#{payload.user.id}`)")
bot!.create_message(config[:leave_channel], out_string)
BOT.create_message(config[:leave_channel], out_string)
end
LOG.info("Loaded JoinLeaveLog Module")


+ 8
- 10
src/modules/Killfile.cr View File

@ -2,21 +2,19 @@ module Killfile
# This module handles guildwide self-blocks.
extend self
@@killfile : Array(UInt64) = load_killfile
def load_killfile
@@killfile : Array(UInt64) = ->{
killfile = [] of UInt64
Bampersand::DATABASE.query "select * from killfile" do |rs|
DATABASE.query "select * from killfile" do |rs|
rs.each do
killfile += [rs.read(Int64).to_u64]
end
end
killfile
end
}.call
def handle_join(payload)
if @@killfile.includes? payload.id.to_u64
bot!.leave_guild(payload.id)
BOT.leave_guild(payload.id)
LOG.info("Guild #{payload.id} is in killfile, leaving again.")
end
end
@ -24,21 +22,21 @@ module Killfile
def add_to_killfile(guild_id)
LOG.info("Adding guild #{guild_id} to killfile.")
@@killfile << guild_id
Bampersand::DATABASE.exec "insert into killfile (guild_id) values (?)", guild_id.to_i64
channels = cache!.channels.values.select { |channel|
DATABASE.exec "insert into killfile (guild_id) values (?)", guild_id.to_i64
channels = CACHE.channels.values.select { |channel|
(channel.guild_id || 0).to_u64 == guild_id
}
send_success = false
channels.each do |channel|
next if send_success
begin
bot!.create_message(channel.id, "The Bot operator is no longer comfortable with you using their services. This decision is final. Have a nice day.")
BOT.create_message(channel.id, "The Bot operator is no longer comfortable with you using their services. This decision is final. Have a nice day.")
send_success = true
rescue e
end
end
LOG.info("Sent goodbye message to #{guild_id}") if send_success
bot!.leave_guild(guild_id)
BOT.leave_guild(guild_id)
LOG.info("Left guild #{guild_id}.")
end


+ 4
- 4
src/modules/Mirroring.cr View File

@ -7,13 +7,13 @@ module Mirroring
# The event handler calls this.
def handle_message(msg)
client = bot!
client = BOT
guild = msg.guild_id
return unless State.feature? guild, State::Features::Mirror
return unless msg.channel_id == State.get(guild)[:mirror_in]
return unless Config.feature? guild, Config::Features::Mirror
return unless msg.channel_id == Config.get(guild)[:mirror_in]
begin
client.create_message(
State.get(guild)[:mirror_out], "", embed: format_message(msg)
Config.get(guild)[:mirror_out], "", embed: format_message(msg)
)
rescue e
LOG.error "Failed to mirror message #{msg.id}: #{e}"


+ 22
- 24
src/modules/ModTools.cr View File

@ -5,57 +5,55 @@ module ModTools
# Tries to get the mute role for a guild
def mute_role?(guild_id)
mute_role_id = cache!.guild_roles[guild_id].find do |role_id|
cache!.resolve_role(role_id).name == "B& Muted"
mute_role_id = CACHE.guild_roles[guild_id].find do |role_id|
CACHE.resolve_role(role_id).name == "B& Muted"
end
return cache!.resolve_role(mute_role_id) unless mute_role_id.nil?
return CACHE.resolve_role(mute_role_id) unless mute_role_id.nil?
nil
end
# Creates a new role, override-denies write permissions for all channels B&
# can see, and raises as far to the top as possible.
def create_mute_role(guild_id)
mute_role = bot!.create_guild_role(guild_id, "B& Muted")
cache!.guild_channels(guild_id).each do |channel_id|
bot!.edit_channel_permissions(
mute_role = BOT.create_guild_role(guild_id, "B& Muted")
CACHE.guild_channels(guild_id).each do |channel_id|
BOT.edit_channel_permissions(
channel_id, mute_role.id, "role",
Discord::Permissions::None, Discord::Permissions::SendMessages
)
end
current_user = cache!.resolve_current_user
member = cache!.resolve_member(guild_id, current_user.id)
current_user = CACHE.resolve_current_user
member = CACHE.resolve_member(guild_id, current_user.id)
position = member.roles.map do |role_id|
cache!.resolve_role(role_id).position
CACHE.resolve_role(role_id).position
end.max
bot!.modify_guild_role_positions(
BOT.modify_guild_role_positions(
guild_id,
[Discord::REST::ModifyRolePositionPayload.new(mute_role.id, position)]
)
cache!.cache(mute_role)
cache!.add_guild_role(guild_id, mute_role.id)
CACHE.cache(mute_role)
CACHE.add_guild_role(guild_id, mute_role.id)
mute_role
end
# Maps Channel-ID => Cooldown in sec
@@slowmodes : Hash(UInt64, UInt32) = load_slowmodes
def load_slowmodes
@@slowmodes : Hash(UInt64, UInt32) = ->{
slowmodes = {} of UInt64 => UInt32
Bampersand::DATABASE.query "select * from slowmodes" do |rs|
DATABASE.query "select * from slowmodes" do |rs|
rs.each do
slowmodes[rs.read(Int64).to_u64] = rs.read(Int64).to_u32
end
end
slowmodes
end
}.call
# Maps Channel-ID => (User-Id => Timestamp)
@@last_msgs = {} of UInt64 => Hash(UInt64, Time)
def set_channel_slowmode(channel_id, secs)
@@slowmodes[channel_id] = secs
@@last_msgs[channel_id] = {} of UInt64 => Time
Bampersand::DATABASE.exec(
@@slowmodes[channel_id.to_u64] = secs
@@last_msgs[channel_id.to_u64] = {} of UInt64 => Time
DATABASE.exec(
"insert into slowmodes values (?, ?)", channel_id.to_i64, secs.to_i64
)
end
@ -63,7 +61,7 @@ module ModTools
def remove_channel_slowmode(channel_id)
@@slowmodes.delete(channel_id)
@@last_msgs.delete(channel_id)
Bampersand::DATABASE.exec(
DATABASE.exec(
"delete from slowmodes where channel_id == ?", channel_id.to_i64
)
end
@ -89,9 +87,9 @@ module ModTools
LOG.debug("Enforcing slowmode on message #{msg.id} by #{msg.author.tag} in #{msg.channel_id}. RIP.")
timeout = (msg.timestamp - last_timestamp - Time::Span.new(0, 0, cooldown)).abs
begin
bot!.delete_message(msg.channel_id, msg.id)
dm = bot!.create_dm(msg.author.id).id
bot!.create_message(
BOT.delete_message(msg.channel_id, msg.id)
dm = BOT.create_dm(msg.author.id).id
BOT.create_message(
dm,
"Your message in <##{msg.channel_id}> has been removed due to slowmode enforcement. Here's the text in case you want to post in at least #{timeout.total_milliseconds/1000} seconds:",
# Posting it as embed circumvents the 2000 char limit.


+ 11
- 13
src/modules/RoleKiosk.cr View File

@ -4,11 +4,9 @@ module RoleKiosk
extend self
# Maps Message ID to Reaction string and associated RoleID
@@role_kiosks : Hash(UInt64, Hash(String, UInt64)) = load_kiosks
def load_kiosks
@@role_kiosks : Hash(UInt64, Hash(String, UInt64)) = ->{
kiosks = {} of UInt64 => Hash(String, UInt64)
Bampersand::DATABASE.query(
DATABASE.query(
"select message_id, data from role_kiosks"
) do |rs|
rs.each do
@ -25,10 +23,10 @@ module RoleKiosk
end
end
kiosks
end
}.call
def update_kiosk(message_id, data_string)
Bampersand::DATABASE.exec("insert into role_kiosks (message_id, data) values (?,?)", message_id.to_i64, data_string)
DATABASE.exec("insert into role_kiosks (message_id, data) values (?,?)", message_id.to_i64, data_string)
emojis = [] of String
roles = [] of UInt64
data_string.split(";") { |arg|
@ -44,33 +42,33 @@ module RoleKiosk
end
def delete_kiosk(message_id)
Bampersand::DATABASE.exec("delete from role_kiosks where message_id = ?", message_id.to_i64)
DATABASE.exec("delete from role_kiosks where message_id = ?", message_id.to_i64)
@@role_kiosks.delete(message_id)
end
def handle_reaction_add(payload)
return if cache!.resolve_user(payload.user_id).bot
return if CACHE.resolve_user(payload.user_id).bot
lookup = @@role_kiosks[payload.message_id.to_u64]?
return unless lookup
target_role = lookup[Util.reaction_to_s(payload.emoji)]?
return unless target_role
LOG.info("Adding Role #{target_role} in #{payload.guild_id} to #{cache!.resolve_user(payload.user_id).tag}")
LOG.info("Adding Role #{target_role} in #{payload.guild_id} to #{CACHE.resolve_user(payload.user_id).tag}")
begin
bot!.add_guild_member_role(payload.guild_id.not_nil!.to_u64, payload.user_id.to_u64, target_role)
BOT.add_guild_member_role(payload.guild_id.not_nil!.to_u64, payload.user_id.to_u64, target_role)
rescue e
LOG.error("Error while adding role: #{e}")
end
end