require_relative '../plugin'
attr_accessor :log_thread, :buffer, :buffer_mutex
match /(.*)/i, :use_prefix => false, :method => :onmessage
listen_to :quitting, :onquit
# Moderation actions (bot-only)
match /(http|https):\/\/([\w\-_]+(?:(?:\.[\w\-_]+)+))([\w\-\.,@?^=%&:\~\+#]*[\w\-\@?^=%&~\+#])?/i, :use_prefix => false, :method => :checksites
match /(.*fuck|.*shit\b|.*bitch|.*\bnigg|.*\bcunt\b|.*\bgtfo\b|.*\bstfu\b|.*\bwtf\b|.*\bfml\b|.*\bbs\b|.*\bidfk\b|.*\bidfc\b|.*\btf\b|.*\baf\b)/i, :use_prefix => false, :method => :warnswears
# Switch actions (op-only)
match /^(!off)/i, :use_prefix => false, :method => :deactivate
match /^(!on)/i, :use_prefix => false, :method => :activate
match /^(!rron)/i, :use_prefix => false, :method => :rron
match /^(!rroff)/i, :use_prefix => false, :method => :rroff
match /^(!tellon)/i, :use_prefix => false, :method => :tellon
match /^(!telloff)/i, :use_prefix => false, :method => :telloff
match /^(!seenon)/i, :use_prefix => false, :method => :seenon
match /^(!seenoff)/i, :use_prefix => false, :method => :seenoff
# Force actions (op-only)
match /^(!kick) (.+)/i, :use_prefix => false, :method => :kick
match /^(!ban) ([^ ]+) (.+)/i, :use_prefix => false, :method => :ban
match /^(!restart)/i, :use_prefix => false, :method => :restart
match /^(!log)/i, :use_prefix => false, :method => :log
match /^(!broadcast) ([^ ]+) ([^ ]+) ([^ ]+) (.+)/i, :use_prefix => false, :method => :broadcast
match /^(!activate) ([^ ]+)/i, :use_prefix => false, :method => :broadcaston
match /^(!deactivate) ([^ ]+)/i, :use_prefix => false, :method => :broadcastoff
match /^(!cleartell) (.+)/i, :use_prefix => false, :method => :cleartell
match /^(!redeem) (.+)/i, :use_prefix => false, :method => :redeem
match /^(!ignore) (.+)/i, :use_prefix => false, :method => :ignore
match /^(!unignore) (.+)/i, :use_prefix => false, :method => :unignore
# Utility actions (universally accepted)
match /^(!tell) ([^ ]+) (.+)/i, :use_prefix => false, :method => :tell
match /^(!seen) (.+)/i, :use_prefix => false, :method => :seen
# RR actions (universally accepted)
match /(^!rrspin)/i, :use_prefix => false, :method => :rrspin
match /(^!rrend)/i, :use_prefix => false, :method => :rrend
match /(^!rrbegin) (.+)/i, :use_prefix => false, :method => :rrbegin
match /(^!rradd) (.+)/i, :use_prefix => false, :method => :rradd
match /(^!rrjoin)/i, :use_prefix => false, :method => :rrjoin
match /(^!rrshoot)/i, :use_prefix => false, :method => :rr
match /(^!rrremove) (.+)/i, :use_prefix => false, :method => :rrremove
match /(^!rrquit)/i, :use_prefix => false, :method => :rrquit
# Bot class initialization.
@log_title = 'Project:Chat/Logs/'
@log_category = 'Wikia Chat logs'
@client.config['logging'] = {
'log_interval' => @log_interval,
'log_title' => @log_title,
'log_category' => @log_category
@log_interval = @client.config['logging']['log_interval']
@log_title = @client.config['logging']['log_title']
@log_category = @client.config['logging']['log_category']
# Logging thread variables.
@buffer_mutex = Mutex.new
@log_thread = make_thread
@client.config['switches'] = {
@silent_on = @client.config['switches']['silent']
@rr_off = @client.config['switches']['rr']
@tell_off = @client.config['switches']['tell']
@seen_off = @client.config['switches']['seen']
# Spam/swear checking YAML stuff.
if File.exists? 'kicks.yml'
@kicks = YAML::load_file 'kicks.yml'
File.open('kicks.yml', 'w+') {|f| f.write({}.to_yaml)}
# Moderation statistic variables.
# Tell command YAML stuff.
if File.exists? 'tells.yml'
@tells = YAML::load_file 'tells.yml'
File.open('tells.yml', 'w+') {|f| f.write({}.to_yaml)}
# Seen command YAML stuff.
if File.exists? 'seen.yml'
@seen = YAML::load_file 'seen.yml'
File.open('seen.yml', 'w+') {|f| f.write({}.to_yaml)}
# Broadcast command YAML stuff.
if File.exists? 'broadcasts.yml'
@broadcasts = YAML::load_file 'broadcasts.yml'
File.open('broadcasts.yml', 'w+') {|f| f.write({}.to_yaml)}
@rr_player_list = Array.new
# Utility only function, revives log thread, clears/flushes buffer, and uploads chat logs.
if File.exists? "#{Time.now.utc.strftime(CATEGORY_TS)}.log"
@file = File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "r+")
@stat_lines = @buffer.scan(/\n/).size
title = "#{@client.config['logging']['log_title']}#{Time.now.utc.strftime(CATEGORY_TS)}"
text = @buffer.dup.gsub('').gsub('_', ' ') # Ideally, this is inside a buffer lock somewhere...
text = "[[Category:#{@client.config['logging']['log_category']}]]
@client.api.edit title, text, :bot => 1, :minor => 0, :summary => "Updating chat logs. #{@stat_kicks} kicks, #{@stat_bans} bans reported."
@file = File.open("#{@old_log}", "r")
title = "#{@client.config['logging']['log_title']}#{Time.now.utc.strftime(CATEGORY_TS)}"
text = @buffer.dup.gsub('').gsub('_', ' ') # Ideally, this is inside a buffer lock somewhere...
text = "[[Category:#{@client.config['logging']['log_category']}]]
@client.api.edit title, text, :bot => 1, :minor => 0, :summary => "Updating chat logs. #{@stat_kicks} kicks, #{@stat_bans} bans reported."
@file = File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "w+")
@old_log = "#{Time.now.utc.strftime(CATEGORY_TS)}.log"
# Utility only. Acts as a watchdog for the logging thread.
thr = Thread.new(@client.config['logging']) {
sleep @client.config['logging']['log_interval']
# Utility only. Passes buffer to a new thread to prevent the input from stalling.
def update_thread(in_thr=false)
@log_thread.kill unless in_thr
@log_thread = make_thread
# Utility only, used to get a detailed timestamp.
ret += "#{decades} decade#{decades > 1 ? 's' : ''}, " if decades > 0
ret += "#{years} year#{years > 1 ? 's' : ''}, " if years > 0
ret += "#{months} month#{months > 1 ? 's' : ''}, " if months > 0
ret += "#{weeks} week#{weeks > 1 ? 's' : ''}, " if weeks > 0
ret += "#{days} day#{days > 1 ? 's' : ''}, " if days > 0
ret += "#{hours} hour#{hours > 1 ? 's' : ''}, " if hours > 0
ret += "#{minutes} minute#{minutes > 1 ? 's' : ''}, " if minutes > 0
ret.gsub(/, @/, ' and ') + "#{ts} second#{ts != 1 ? 's' : ''}"
# Gets page queries for logging. Utility only.
def get_page_contents(title)
"http://#{@client.config['wiki']}.wikia.com/index.php",
@client.config['logging']['title'] => title,
thr = Thread.new(@broadcasts[index]) {
#sleep @broadcasts[index]['interval']
@client.send_msg @broadcasts[index]['message']
@client.send_msg @broadcasts[index]['active']
@client.send_msg repetitions
if @broadcasts[index]['active'] == false or repetitions >= @broadcasts[index]['repetitions']
# What the bot does when she receives a message.
def onmessage(user, message)
@buffer_mutex.synchronize do
message.split(/\n/).each do |line|
if /^\/me/.match(line) and message.start_with? '/me'
File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "a") {|f| f.write("\n" + Time.now.utc.strftime(MESSAGE_TS) + ' [CHAT] ' + "#{user.log_name.gsub("_", ' ')} #{line.gsub(/\/me /, '')}")}
puts Time.now.utc.strftime(MESSAGE_TS) + ' [CHAT] ' + "#{user.log_name.gsub("_", ' ')} #{line.gsub(/\/me /, '')}"
elsif message.start_with? '/me'
File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "a") {|f| f.write("\n" + Time.now.utc.strftime(MESSAGE_TS) + ' [CHAT] ' + "#{user.log_name.gsub("_", ' ')} #{line.gsub(/\/me /, '')}")}
puts Time.now.utc.strftime(MESSAGE_TS) + ' [CHAT] ' + "#{user.log_name.gsub("_", ' ')} #{line.gsub(/\/me /, '')}"
File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "a") {|f| f.write("\n" + Time.now.utc.strftime(MESSAGE_TS) + ' [CHAT] ' + "#{user.log_name.gsub("_", ' ')}: #{line}")}
puts Time.now.utc.strftime(MESSAGE_TS) + ' [CHAT] ' + "#{user.log_name.gsub("_", ' ')}: #{line}"
@seen[user.name.downcase] = Time.now.to_i
File.open('seen.yml', File::WRONLY) {|f| f.write(@seen.to_yaml)}
# What the bot does when someone joins.
name = args[0]['attrs']['name'].gsub("_", ' ')
edits = args[0]['attrs']['editCount']
@buffer_mutex.synchronize do
File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "a") {|f| f.write("\n" + Time.now.utc.strftime(MESSAGE_TS) + ' [JOIN] ' + "#{name} has joined the chat.")}
puts Time.now.utc.strftime(MESSAGE_TS) + ' [JOIN] ' + "#{name} has joined the chat."
@client.send_msg "#{name}, you need at least 1 edit to participate in the chat, please leave and get an edit before coming back."
@kicks[name.downcase] = 1
@kicks[name.downcase] = @kicks[name.downcase] + 1
elsif @kicks[name.downcase] >= 2
@client.ban(name, 86400, "User sweared.")
@kicks[name.downcase] = 0
File.open('kicks.yml', File::WRONLY) {|f| f.write(@kicks.to_yaml)}
@tell_mutex.synchronize do
if @tells.has_key? name.downcase
@tells[name.downcase].each do |k,v|
@client.send_msg "#{name}, #{k} wanted me to tell you: #{v}"
@tells[name.downcase] = {}
File.open('tells.yml', 'w+') {|f| f.write(@tells.to_yaml)}
@seen[name.downcase] = Time.now.to_i
File.open('seen.yml', File::WRONLY) {|f| f.write(@seen.to_yaml)}
# What the bot does when someone leaves.
@buffer_mutex.synchronize do
File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "a") {|f| f.write("\n" + Time.now.utc.strftime(MESSAGE_TS) + ' [QUIT] ' + "#{data['attrs']['name'].gsub("_", ' ')} has left the chat.")}
puts Time.now.utc.strftime(MESSAGE_TS) + ' [QUIT] ' + "#{data['attrs']['name'].gsub("_", ' ')} has left the chat."
# What the bot does when she exits or updates.
# What the bot does when someone is kicked.
@buffer_mutex.synchronize do
File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "a") {|f| f.write("\n" + Time.now.utc.strftime(MESSAGE_TS) + ' [KICK] ' + "#{data['attrs']['kickedUserName'].gsub("_", ' ')} was kicked from chat by #{data['attrs']['moderatorName'].gsub("_", ' ')}.")}
puts Time.now.utc.strftime(MESSAGE_TS) + ' [KICK] ' + "#{data['attrs']['kickedUserName'].gsub("_", ' ')} was kicked from chat by #{data['attrs']['moderatorName'].gsub("_", ' ')}."
# What the bot does when someone is banned.
@buffer_mutex.synchronize do
File.open("#{Time.now.utc.strftime(CATEGORY_TS)}.log", "a") {|f| f.write("\n" + Time.now.utc.strftime(MESSAGE_TS) + ' [CBAN] ' + "#{data['attrs']['kickedUserName'].gsub("_", ' ')} was banned from chat by #{data['attrs']['moderatorName'].gsub("_", ' ')}.")}
puts Time.now.utc.strftime(MESSAGE_TS) + ' [CBAN] ' + "#{data['attrs']['kickedUserName'].gsub("_", ' ')} was banned from chat by #{data['attrs']['moderatorName'].gsub("_", ' ')}."
# Enables bot speech and AI.
return if @silent_on == false or !user.is? :op
@client.config['switches']['silent'] = false
@client.send_msg "I am no longer in Silent Mode."
# Disables bot speech and AI; functions still work, just don't relay feedback.
def deactivate(user, text)
return if @silent_on == true or !user.is? :op
@client.config['switches']['silent'] = true
@client.send_msg "I am now going into Silent Mode."
# Enabled Russian Roulette; recommended during dead chat events.
return if @rr_off == false or !user.is? :op
@client.send_msg "Russian Roulette now enabled."
@client.config['switches']['rr'] = false
@rr_rounds_left = @rr_rounds
@rr_player_list = Array.new
# Disables Russian Roulette; recommended during active chat events or when spam occurs.
return if @rr_off == true or !user.is? :op
@client.send_msg "Russian Roulette now disabled."
@client.config['switches']['rr'] = true
@rr_rounds_left = @rr_rounds
@rr_player_list = Array.new
# Enables telling, bypasses silent mode if a message is already in someone's inbox.
return if @tell_off == false or !user.is? :op
@client.send_msg "Telling is now enabled!"
@client.config['switches']['tell'] = false
# Disables telling, messages in inbox are still sent freely.
return if @tell_off == true or !user.is? :op
@client.send_msg "Telling is now disabled!"
@client.config['switches']['tell'] = true
# Enables seeing, might lead to spam.
return if @seen_off == false or !user.is? :op
@client.send_msg "Seen is now enabled!"
@client.config['switches']['seen'] = false
# Disables seeing, bot still keeps time updated.
return if @seen_off == true or !user.is? :op
@client.send_msg "Seen is now disabled!"
@client.config['switches']['seen'] = true
# Ignores a user, except for swearing, logs, and general events.
def ignore(user, text, target)
@client.send_msg "I am now ignoring all messages from #{target}."
if @client.userlist.key? target
@client.userlist[target].ignore
def unignore(user, text, target)
@client.send_msg "I am no longer ignoring messages from #{target}."
if @client.userlist.key? target
@client.userlist[target].unignore
User.new(target).unignore
# Kicks a user, used specifically as a last-chance reach-around for Akrivus or an informal kick approach.
def kick(user, text, target)
target = target.gsub("_", ' ')
@client.send_msg "Kicking #{target}."
# Bans a user, used for the same intents as kick, and bans in minutes.
def ban(user, text, target, time)
target = target.gsub("_", ' ')
if time == "infinite" or time == "forever" or time == "perma" or time == "indefinite"
elsif time == "unban" or time == "end"
@client.send_msg "#{time == 0 ? "Unbanning #{target}." : "Banning #{target} for #{get_hms(time * 60)}."}"
@client.ban(target, time * 60, "Banned by bot for immediate policy infractions.")
def broadcast(user, text, index, interval, repetitions, message)
return if !user.is? :op or @broadcasts.has_key? index
repetitions = repetitions.to_i
@broadcasts[index] = {'message' => message, 'interval' => interval, 'repetitions' => repetitions, 'active' => true}
File.open('broadcasts.yml', File::WRONLY) {|f| f.write(@broadcasts.to_yaml)}
@client.send_msg "I will now broadcast '#{message}' every #{get_hms(interval)} for #{repetitions} repetitions under name #{index}."
# Activates a specific broadcast.
def broadcaston(user, text, index)
return if !user.is? :op or @broadcasts.has_key? index or @broadcasts[index]['active'] == true
@broadcasts[index]['active'] = true
File.open('broadcasts.yml', File::WRONLY) {|f| f.write(@broadcasts.to_yaml)}
# Deactivates a specific broadcast.
def broadcastoff(user, text, index)
return if !user.is? :op or @broadcasts.has_key? index or @broadcasts[index]['active'] == false
@broadcasts[index]['active'] = false
File.open('broadcasts.yml', File::WRONLY) {|f| f.write(@broadcasts.to_yaml)}
# Clears the inbox of a user, used to prevent spam.
def cleartell(user, text, target)
target = target.gsub("_", ' ')
target = target.gsub("[", '')
target = target.gsub("]", '')
@client.send_msg "Cleared user #{target}'s infobox."
@tell_mutex.synchronize do
@tells[target.downcase] = {}
File.open('tells.yml', 'w+') {|f| f.write(@tells.to_yaml)}
# Clears the kick count of a user to prevent accidental bans.
def redeem(user, text, target)
target = target.gsub("_", ' ')
target = target.gsub("[", '')
target = target.gsub("]", '')
@client.send_msg "Cleared user #{target} of all detected infractions."
@kicks[target.downcase] = 0
File.open('kicks.yml', File::WRONLY) {|f| f.write(@kicks.to_yaml)}
# Uploads a new chat log. Used if force is needed, also used to test.
@buffer_mutex.synchronize do
@client.send_msg "[[Project:Chat/Logs|Logs]] have been updated."
# Restarts the bot, originally !quit but renamed to prevent confusion.
# Tell command, puts a message into a user's inbox and fires it whenever the recipient joins.
def tell(user, text, target, message)
return if @tell_off == true
target = target.gsub("_", ' ')
target = target.gsub("[", '')
target = target.gsub("]", '')
if target.downcase.eql? user.name.downcase
return @client.send_msg "You can't send a message to yourself, #{user.name}!"
elsif target.downcase.eql? @client.config['user'].downcase
return @client.send_msg "Thank you for the message, #{user.name}."
elsif [email protected][:allow_tell_to_present_users] and @client.userlist.keys.collect {|name| name.downcase}.include? target.downcase
return @client.send_msg "#{user.name}, that user is already online!"
@tell_mutex.synchronize do
if @tells.has_key? target.downcase
@tells[target.downcase][user.name] = message
@tells[target.downcase] = {user.name => message}
File.open('tells.yml', File::WRONLY) {|f| f.write(@tells.to_yaml)}
return if @silent_on == true
@client.send_msg "I'll tell #{target} that the next time I see them, #{user.name}."
# Seen command, gives a detailed time stamp of when a user was last online.
def seen(user, text, target)
return if @silent_on == true or @seen_off == true
target = target.gsub("_", ' ')
target = target.gsub("[", '')
target = target.gsub("]", '')
if @client.userlist.keys.collect {|name| name.downcase}.include? target.downcase and [email protected][:seen_use_last_post]
@client.send_msg "#{user.name}, they're here right now!"
elsif @seen.key? target.downcase
@client.send_msg "#{user.name}, I last saw #{target} around #{get_hms(Time.now.to_i - @seen[target.downcase])} ago."
@client.send_msg "#{user.name}, I haven't seen #{target}."
# Begins a new game of Russian Roulette, assuming that the game is allowed.
def rrbegin(user, users, text)
return if @silent_on == true or @rr_off == true or @rr_game_started == true
users = user.name + "," + users
users = users.gsub("_", ' ')
users = users.gsub("[", '')
users = users.gsub("]", '')
@client.send_msg "#{user.name} has started a game of Russian Roulette!"
@rr_rounds_left = @rr_rounds
@rr_player_list = users.split(",")
@rr_player_list.delete("!rrbegin")
@rr_player_host = user.name
def rradd(user, text, target)
return if @silent_on == true or @rr_off == true or @rr_game_started == false
if @rr_player_host == user.name or user.is? :op
users = users.gsub("_", ' ')
users = users.gsub("[", '')
users = users.gsub("]", '')
@client.send_msg "#{target} has been added to the game!"
@rr_player_list += users.split(",")
@rr_player_list.delete("!rradd")
return if @silent_on == true or @rr_off == true or @rr_game_started == false
@client.send_msg "#{user.name} has joined the game!"
@rr_player_list.push(user.name)
return if @silent_on == true or @rr_off == true or @rr_game_started == false
@client.send_msg "#{user.name} has quit the game!"
@rr_player_list.delete(user.name)
def rrremove(user, text, target)
return if @silent_on == true or @rr_off == true or @rr_game_started == false
if @rr_player_host == user.name or user.is? :op
target = target.gsub("_", ' ')
target = target.gsub("[", '')
target = target.gsub("]", '')
targets = target.split(",")
@client.send_msg "#{target} has been removed from the game!"
@rr_player_list.delete(n)
return if @silent_on == true or @rr_off == true or @rr_game_started == false
if @rr_player_host == user.name or user.is? :op
@client.send_msg "#{user.name} has ended the game of Russian Roulette!"
@rr_rounds_left = @rr_rounds
@rr_player_list = Array.new
return if @silent_on == true or @rr_off == true or @rr_game_started == false
if @rr_player_target == user.name or @rr_player_host == user.name or user.is? :op
chance = rand(@rr_rounds_left)
@client.send_msg "#{user.name} pulls the trigger and dies, ending the game. #{user.name} lost, everyone else won."
@rr_rounds_left = @rr_rounds
@rr_player_list = Array.new
@client.send_msg "#{user.name} pulls the trigger and nothing happens. Spin the gun again by typing !rrspin"
@rr_rounds_left = @rr_rounds_left - 1
return if @silent_on == true or @rr_off == true or @rr_game_started == false
if @rr_player_target == user.name or @rr_player_host == user.name or user.is? :op
pi = rand(@rr_player_list.length)
@rr_player_target = @rr_player_list.at(pi)
@client.send_msg "#{user.name} has spun the gun. It is pointed straight at #{@rr_player_target}! Type !rrshoot to continue."
# Moderation code, uses Web Of Trust (WOT) foundation API to find if a site is child friendly and secure.
def checksites(user, prot, url, slash)
return if /.*imgur.com|.*shamchat.com|.*thatsthefinger.com/i.match(url) != nil
res = HTTParty.get("http://api.mywot.com/0.4/public_link_json2?hosts=#{url}/&key=26440f798f13affc273dad8d1f40b43684651373")
cf = data["#{url}"]["0"][0]
tw = data["#{url}"]["4"][0]
@client.send_msg "#{user.name}, that site contains content that violates our rules."
@client.ban(user.name, 86400, "Linked an inappropriate website to chat.")
@client.send_msg "#{user.name}, that site contains content that violates our rules."
# Moderation code, semi-Scunthorpe safe code that checks for swears. GEMFUCK is allowed.
def warnswears(user, text)
return if /.*\bgemfuck\b/i.match(text) != nil
@client.send_msg "#{user.name}, please don't swear!"
@kicks[user.name.downcase] = 1
@kicks[user.name.downcase] = @kicks[user.name.downcase] + 1
if @kicks[user.name.downcase]
elsif @kicks[user.name.downcase] >= 2
@client.ban(user.name, 7200, "User sweared.")
@kicks[user.name.downcase] = 0
File.open('kicks.yml', File::WRONLY) {|f| f.write(@kicks.to_yaml)}