aggixx

WoW Kitsunebot Scripts

Apr 15th, 2015
386
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 19.05 KB | None | 0 0
  1. --[[ ==============================================================================================
  2.                     WoW Kitsunebot Script
  3. ===================================================================================================
  4.                written by twitch.tv/aggixx | questions to @PawketsWoW
  5.    
  6.    How to setup Kitsunebot:
  7.      1) Go to http://www.twitch.tv/kitsunebot and type !join to make Kitsunebot join your channel.
  8.      2) In your channel, set this to the startup script using the following chat command:
  9.         !kitsunebot MOD.StartupScript( "86bgZ2jJ" ); MOD.Reload();
  10.      3) Use MOD.SetVar to set the required options described in the following sections. Ex:
  11.         !kitsunebot MOD.SetVar( "armoryDefaultRegion", "us" );
  12.         !kitsunebot MOD.SetVar( "guild", "Huge in Japan", "WarcraftLogs" );
  13.  
  14.    ===================================  Armory Info Command =======================================
  15.    !armory (US|EU) Character-Realm  ex: !armory US Pawketz-Kil'jaeden
  16.       Returns information from the armory about the specified character. The US/EU parameter is
  17.       optional if var "armoryDefaultRegion" is set. You can set a character for the command to
  18.       default to when all parameters are ommitted by setting var "armoryDefaultCharacter".
  19.  
  20.   ================================== WarcraftLogs Log Tracking ====================================
  21.   To use the tracking you must first set the following variables in the "WarcraftLogs" table:
  22.    - "apiKey" - You can get an API key at the bottom of the settings page on warcraftlogs.com, use
  23.      the public key.
  24.    - "character" - Name of the character you would like to track for ranks (currently unused but
  25.      you must set it anyway). Just char name, no
  26.      realm.
  27.    - "guild" - WCL guild name to use for log auto-select (you can always explicitly track a log on
  28.      a different guild).
  29.    - "realm" - Realm of said guild.
  30.    - "region" - Region of said guild.
  31.   Tracking a log:
  32.      Use "!wcl init" to select a log. You can explicitly pick a log by passing the log ID as a
  33.      parameter, otherwise Kitsunebot will present a list of recently active logs from your guild's
  34.      page and you can pick one. If there's only 1 recently active log it will automatically choose
  35.      it for you.
  36.      Once you've selected a log, tracking is now active and Kitsunebot will announce and link to
  37.      your boss kills as they happen. Tracking will automatically turn off if your stream is offline
  38.      for more than 2 minutes, and you can also use "!wcl (on|off)" to manually toggle tracking.
  39. --]]
  40.  
  41. if not ( MOD.Timer and MOD.GetStream ) then
  42.     MOD.Ticks=0;
  43.      
  44.     --timer
  45.     local timerevents = {};
  46.            
  47.     --example tick function
  48.     local function Tick(channel)
  49.         if type(MOD.EventTick) == "function" then
  50.             local ret = MOD.EventTick(user,msg);
  51.             if ret ~= nil then
  52.                 return ret;
  53.             end
  54.         end
  55.      
  56.         MOD.Ticks = MOD.Ticks + 1;
  57.      
  58.         local now = os.time();
  59.      
  60.         for k,v in pairs(timerevents)do
  61.             if v.start + v.interval <= now then
  62.                 if v.action ~= nil then
  63.                     if v.action ~= nil then
  64.                         if type(v.action) == "function" then
  65.                             v.action();
  66.                             return;
  67.                         else
  68.                             print(tostring(v.action));
  69.                         end
  70.                     end
  71.                 end
  72.      
  73.                 if v.recurring then
  74.                     v.start = v.start + v.interval;
  75.                 else
  76.                     timerevents[k] = nil;
  77.                 end
  78.             end
  79.         end
  80.     end
  81.      
  82.     function MOD.GetTimers()
  83.         local list = {};
  84.         for k,v in pairs(timerevents)do
  85.             table.insert(list,k);
  86.         end
  87.      
  88.         return list;
  89.     end
  90.      
  91.     function MOD.Timer( name, seconds, action, recurring )
  92.         if action == nil then
  93.             timerevents[name] = nil;
  94.             return;
  95.         end
  96.            
  97.         if recurring == nil then
  98.             recurring = false;
  99.         elseif type( recurring ) ~= "boolean" then
  100.             error( "Recurring must be a boolean value" );
  101.             return;
  102.         end
  103.      
  104.         if type(action) ~= "function" and type(action) ~= "string" then
  105.             error("Action must be a function or a string");
  106.             return;
  107.         end
  108.      
  109.         timerevents[name] = {
  110.             ["start"] = os.time(),
  111.             ["action"] = action,
  112.             ["recurring"] = recurring,
  113.             ["interval"] = seconds,
  114.         };
  115.     end
  116.      
  117.     MOD.SetTickEvent(Tick);
  118.      
  119.     function MOD.GetStream(name)
  120.      
  121.         local body, code, headers, status = MOD.HTTPSGet("https://api.twitch.tv/kraken/streams/"..name);
  122.      
  123.         if body == nil then
  124.             return nil,"not found";
  125.         end
  126.      
  127.         local raw = JSONDecode(body);
  128.      
  129.         if raw == nil then
  130.             return nil,"could not decode response";
  131.         elseif raw.error ~= nil then
  132.             return nil,tostring(raw.error);
  133.         elseif type(raw.stream) ~= "table" then
  134.             return nil,"offline";
  135.         end
  136.      
  137.         local ret = {};
  138.         ret.viewers = raw.stream.viewers;
  139.         ret.game = raw.stream.game;
  140.         ret.name = raw.stream.channel.display_name;
  141.         ret.partner = raw.stream.channel.partner;
  142.         ret.views = raw.stream.channel.views;
  143.         ret.language = raw.stream.channel.language;
  144.         ret.mature = raw.stream.channel.mature;
  145.         ret.status = raw.stream.channel.status;
  146.         ret.started = raw.stream.created_at;
  147.         ret.fps = raw.stream.average_fps;
  148.         ret.followers = raw.stream.channel.followers;
  149.      
  150.         return ret;
  151.     end
  152. end
  153.  
  154. local classes = { "Warrior", "Paladin", "Hunter", "Rogue", "Priest", "Death Knight", "Shaman", "Mage", "Warlock", "Monk", "Druid" };
  155.  
  156. local races = {
  157.     [1] = "Human",
  158.     [2] = "Orc",
  159.     [3] = "Dwarf",
  160.     [4] = "Night Elf",
  161.     [5] = "Undead",
  162.     [6] = "Tauren",
  163.     [7] = "Gnome",
  164.     [8] = "Troll",
  165.     [9] = "Goblin",
  166.     [10] = "Blood Elf",
  167.     [11] = "Draenei",
  168.     [22] = "Worgen",
  169.     [24] = "Pandaren",
  170. };
  171.  
  172. --[[
  173. --Google URL Shortener API
  174.  
  175. local shorten;
  176. do
  177.     local key = "";
  178.     local http;
  179.     local ltn12;
  180.  
  181.     function shorten( url )
  182.         if not http and ltn12 then
  183.             -- loads the HTTP module and any libraries it requires
  184.             http = require("socket.http");
  185.             ltn12 = require("ltn12");
  186.         end
  187.        
  188.         local request = ;
  189.         local response = {}
  190.         local body, code, headers, status = http.request {
  191.             url = "https://www.googleapis.com/urlshortener/v1/url?key=" .. key,
  192.             method = "POST",
  193.             source = JSONEncode( {
  194.                 ["longUrl"] = url,
  195.             } ),
  196.             headers = {
  197.                 ["Content-Type"] = "application/json",
  198.             },
  199.             sink = ltn12.sink.table( response ),
  200.         };
  201.  
  202.         print('body:' .. tostring(body))
  203.         print('code:' .. tostring(code))
  204.         print('headers:' .. util.tableToString(headers))
  205.         print('status:' .. tostring(status))
  206.     end
  207. end
  208. --]]
  209.  
  210. local function url_encode( str )
  211.   if (str) then
  212.     str = string.gsub (str, "\n", "\r\n")
  213.     str = string.gsub (str, "([^%w %-%_%.%~%?=#:/])",
  214.     function (c) return string.format ("%%%02X", string.byte(c)) end)
  215.     str = string.gsub (str, " ", "+")
  216.   end
  217.   return str   
  218. end
  219.  
  220. MOD.url_encode = url_encode;
  221.  
  222. local function slugify( str )
  223.     return string.gsub( string.gsub( str:lower(), "%s", "-" ), "%A", "" );
  224. end
  225.  
  226. --[[ ================================================================
  227.       !armory command
  228. ================================================================ --]]
  229.  
  230. local function armory( msg )
  231.     local region, character, realm;
  232.     if not msg or string.match( msg, "^%s*$" ) then
  233.         character, realm = string.match( MOD.GetVar( "armoryDefaultCharacter" ), "^([^-%s]+)-(.+%S)%s*$" );
  234.         region = MOD.GetVar( "armoryDefaultRegion" );
  235.     elseif string.match( msg, "^%a%a%s[^-%s]+-.+%S%s*$" ) then
  236.         region, character, realm = string.match( msg, "^(%a%a)%s([^-%s]+)-(.+%S)%s*$" );
  237.         region = region:lower();
  238.     else
  239.         character, realm = string.match( msg, "^([^-%s]+)-(.+%S)%s*$" );
  240.         region = MOD.GetVar( "armoryDefaultRegion" );
  241.     end
  242.  
  243.     if not ( realm and character and region and ( region == "us" or region == "eu" ) ) then
  244.         print("Please specify a valid character, ex: !armory Pawketz-Kil'jaeden");
  245.         return;
  246.     end
  247.  
  248.     local d = MOD.HTTPSGet( "http://" .. region .. ".battle.net/api/wow/character/" .. realm .. "/" .. character .. "?fields=items,titles,talents" );
  249.     d = JSONDecode(d);
  250.  
  251.     if d.status and d.status == "nok" then
  252.         print('Character "' .. character .. '" not found on realm "' .. realm .. '".');
  253.         return;
  254.     end
  255.  
  256.     local title = "%s";
  257.     for i=1,#d.titles do
  258.         if d.titles[i].selected then
  259.             title = d.titles[i].name;
  260.             break;
  261.         end
  262.     end
  263.  
  264.     local spec = "";
  265.     for i=1,#d.talents do
  266.         if d.talents[i].selected then
  267.             spec = " " .. d.talents[i].spec.name;
  268.             break;
  269.         end
  270.     end
  271.    
  272.     print( string.format( "%s | %d %s %s %s | iLvl %d (%d) | %s",
  273.         string.format( title, d.name ),
  274.         d.level,
  275.         races[d.race],
  276.         spec,
  277.         classes[d.class],
  278.         d.items.averageItemLevelEquipped,
  279.         d.items.averageItemLevel,
  280.         url_encode( string.format( "http://%s.battle.net/wow/en/character/%s/%s/advanced", region, slugify( realm ), character ) )
  281.     ) );
  282. end
  283.  
  284. MOD.AddCom("armory", armory);
  285.  
  286. --[[ ================================================================
  287.       WarcraftLogs Tracking
  288. ================================================================ --]]
  289.  
  290. do
  291.     local WCL = {};
  292.     local report;
  293.     local reportSelect = {};
  294.     local activityTimeout = 60*60*2; -- 2 hours
  295.     local trackStatus = false;
  296.     local streamTimeout = 0;
  297.  
  298.     local function getWCLConfig()
  299.         local config = MOD.GetArray( "WarcraftLogs" );
  300.  
  301.         if not ( config.apiKey and config.apiKey ~= "" ) then
  302.             return "bad apiKey";
  303.         elseif not ( config.realm and config.realm ~= "" ) then
  304.             return "bad realm";
  305.         elseif not ( config.region and config.region ~= "" ) then
  306.             return "bad region";
  307.         elseif not ( config.guild and config.guild ~= "" ) then
  308.             return "bad guild";
  309.         elseif not ( config.character and config.character ~= "" ) then
  310.             return "bad character";
  311.         end
  312.  
  313.         return config;
  314.     end
  315.  
  316.     local zoneData, classData, kills;
  317.    
  318.     local function initZoneData( config )
  319.         if not config then
  320.             config = getWCLConfig();
  321.             if ( type(config) == "string" ) then
  322.                 print( config );
  323.                 return;
  324.             end
  325.         end
  326.        
  327.         if not ( zoneData and zoneData ~= {} ) then
  328.             zoneData = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/zones?api_key=%s", config.apiKey ) ) );
  329.         end
  330.     end
  331.    
  332.     local function initClassData( config )
  333.         if not config then
  334.             config = getWCLConfig();
  335.             if ( type(config) == "string" ) then
  336.                 print( config );
  337.                 return;
  338.             end
  339.         end
  340.        
  341.         if not ( classData and classData ~= {} ) then
  342.             classData = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/classes?api_key=%s", config.apiKey ) ) );
  343.         end
  344.     end
  345.    
  346.     local function initTracking( config )
  347.         if not config then
  348.             config = getWCLConfig();
  349.             if ( type(config) == "string" ) then
  350.                 print( config );
  351.                 return;
  352.             end
  353.         end
  354.    
  355.         initClassData( config );
  356.         initZoneData( config );
  357.        
  358.         kills = {};
  359.         local data = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/report/fights/%s?api_key=%s", report, config.apiKey ) ) );
  360.         for i=1,#data.fights do
  361.             if data.fights[i].boss > 0 and data.fights[i].kill and not kills[data.fights[i].id] then
  362.                 local t = data.fights[i];
  363.                 local id = t.id;
  364.                 t.id = nil;
  365.                 kills[id] = t;
  366.             end
  367.         end
  368.    
  369.     end
  370.    
  371.     local function track()
  372.         if MOD.GetStream(string.match(Channel(), "^#(.+)")) then
  373.             streamTimeout = 0;
  374.         else
  375.             streamTimeout = streamTimeout + 1;
  376.             if streamTimeout >= 5 then
  377.                 WCL.setTrackingStatus( false );
  378.                 print( "WCL tracking has been disabled due to stream inactivity." );
  379.                 return;
  380.             end
  381.         end
  382.    
  383.         local config = getWCLConfig();
  384.         if ( type(config) == "string" ) then
  385.             print( config );
  386.             return;
  387.         end
  388.        
  389.         local data = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/report/fights/%s?api_key=%s", report, config.apiKey ) ) );
  390.        
  391.         for i=1,#data.fights do
  392.             if data.fights[i].boss > 0 and data.fights[i].kill and not kills[data.fights[i].id] then
  393.                 local t = data.fights[i];
  394.                 local id = t.id;
  395.                 t.id = nil;
  396.                 kills[id] = t;
  397.                
  398.                 local killSec = ( t.end_time - t.start_time ) / 1000;
  399.                 local timeStr = math.floor( killSec / 60 ) .. "m" .. math.floor( killSec % 60 ) .. "s";
  400.                
  401.                 -- Print slain notice
  402.                 print( string.format( "%s has been slain in %s!", t.name, timeStr ) );
  403.                
  404.                 --[[
  405.                 -- Figure out corresponding zone id from boss id
  406.                 local zone;
  407.                 for j=1,#zoneData do
  408.                     for k=1,#zoneData[j].encounters do
  409.                         if zoneData[j].encounters[k].id == t.boss then
  410.                             zone = zoneData[j].id;
  411.                             break;
  412.                         end
  413.                     end
  414.                     if zone then break end
  415.                 end
  416.                
  417.                 if type(zone) ~= "number" then
  418.                     error( "zone is not a number" );
  419.                     return;
  420.                 end
  421.                
  422.                 -- Pull rank data for boss id
  423.                 local rData = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/rankings/character/%s/%s/%s?zone=%d&metric=dps&limit=5000&api_key=%s", config.character, slugify( config.realm ), config.region, zone, config.apiKey ) ) );
  424.                
  425.                 -- Parse rank data and print rank, if found
  426.                 for j=1,#rData do
  427.                     if rData[j].encounter == t.boss and rData[j].reportID == report then
  428.                         local className, specName;
  429.                         for k=1,#classData do
  430.                             if classData[k].id == rData[j].class then
  431.                                 className = classData[k].name;
  432.                                 for l=1,#classData[k].specs do
  433.                                     if classData[k].specs[l].id == rData[j].spec then
  434.                                         specName = classData[k].specs[l].name;
  435.                                         break;
  436.                                     end
  437.                                 end
  438.                                 break;
  439.                             end
  440.                         end
  441.                        
  442.                         local pct = math.floor( ( 1 - ( rData[j].rank / rData[j].outOf ) ) * 100 + 0.5 );
  443.                    
  444.                         print( string.format( "%s set a new personal best of %d DPS, ranking %d out of %d %s %ss (%d%%)!", config.character, rData[j].total, rData[j].rank, rData[j].outOf, specName, className, pct ) );
  445.                     end
  446.                 end
  447.                 --]]
  448.                
  449.                 -- Print URL to log
  450.                 print( url_encode( string.format( "https://www.warcraftlogs.com/reports/%s#fight=%d", report, id ) ) );
  451.                
  452.                 -- Break out so we only report one kill at a time
  453.                 return;
  454.             end
  455.         end
  456.     end
  457.  
  458.     function WCL.setTrackingStatus( status )
  459.         assert( type(status) == "boolean" );
  460.         if trackStatus ~= status then
  461.             if status then
  462.                 initTracking();
  463.                 MOD.Timer( "WCLtrack", 30, track, true );
  464.             else
  465.                 MOD.Timer( "WCLtrack" );
  466.             end
  467.         end
  468.         trackStatus = status;
  469.     end
  470.  
  471.     local function setActiveReport( r )
  472.         report = r;
  473.         print( report .. " is now the active report." );
  474.         WCL.setTrackingStatus( true );
  475.     end
  476.  
  477.     MOD.AddCom("wcl", function( msg, user )
  478.         if string.match( msg, "^init" ) and UserStatus( user ) == 2 then
  479.             local config = getWCLConfig();
  480.             if ( type(config) == "string" ) then
  481.                 print( config );
  482.                 return;
  483.             end
  484.  
  485.             if string.match( msg, "^init %w+" ) then
  486.                 local id = string.match( msg, "^init (%w+)" );
  487.                 local data = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/report/fights/%s?api_key=%s", id, config.apiKey ) ) );
  488.                
  489.                 if data.error then
  490.                     print( data.status .. ": " .. data.error );
  491.                 else
  492.                     setActiveReport( id );
  493.                 end
  494.             else
  495.                 local data = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/reports/guild/%s/%s/%s?api_key=%s&start=%s", config.guild, slugify( config.realm ), config.region, config.apiKey, ( os.time() - 60*60*24 ) * 1000 ) ) ); -- filter to the last 24 hours
  496.  
  497.                 local activeReports = {};
  498.                 for i=1,#data do
  499.                     if data[i]["end"] > ( os.time() - activityTimeout ) * 1000 then
  500.                         table.insert( activeReports, i );
  501.                     end
  502.                 end
  503.  
  504.                 if #activeReports == 0 then
  505.                     print( "No reports found." );
  506.                 elseif #activeReports == 1 then
  507.                     setActiveReport( data[activeReports[1] ].id );
  508.                 else
  509.                     print( "The following recently active reports were found, please select one (!wcl select x):" );
  510.                     reportSelect = {};
  511.                     for i=1,#activeReports do
  512.                         if i > 1 then
  513.                             print("|");
  514.                         end
  515.                         print( string.format( "%s. %s by %s", i, data[activeReports[i] ].title, data[activeReports[i] ].owner ) );
  516.                         table.insert( reportSelect, data[activeReports[i] ].id );
  517.                     end
  518.                 end
  519.             end
  520.         elseif string.match( msg, "^select %d+$") and UserStatus( user ) == 2 then
  521.             if reportSelect ~= {} then
  522.                 local i = tonumber( string.match( msg, "^select (%d+)$") );
  523.                 setActiveReport( reportSelect[i] );
  524.                 reportSelect = {};
  525.             end
  526.         elseif msg:lower() == "on" and UserStatus( user ) == 2 then
  527.             if report ~= "" then
  528.                 print("WCL tracking is now on.");
  529.                 WCL.setTrackingStatus( true );
  530.             else
  531.                 print("No report is selected. Please use !wcl init");
  532.             end
  533.         elseif msg:lower() == "off" and UserStatus( user ) == 2 then
  534.             if report ~= "" then
  535.                 print("WCL tracking is now off.");
  536.                 WCL.setTrackingStatus( false );
  537.             else
  538.                 print("No report is selected. Please use !wcl init");
  539.             end
  540.         --[[elseif string.match(msg, "^record .+ [NHM]$") and UserStatus( user ) == 2 then
  541.             local name, difficulty = string.match(msg, "^record (.+) ([NHM])$"):lower();
  542.            
  543.             if difficulty == "N" then
  544.                 difficulty = 3;
  545.             elseif difficulty == "H" then
  546.                 difficulty = 4;
  547.             elseif difficulty == "M" then
  548.                 difficulty = 5;
  549.             end
  550.            
  551.             local config = getWCLConfig();
  552.             if ( type(config) == "string" ) then
  553.                 print( config );
  554.                 return;
  555.             end
  556.            
  557.             -- Find the boss' id
  558.             initZoneData( config );
  559.            
  560.             local id, zoneId;
  561.             for i=1,#zoneData do
  562.                 for j=1,#zoneData[i].encounters do
  563.                     if zoneData[i].encounters[j].name:lower() == name then
  564.                         id = zoneData[i].encounters[j].id;
  565.                         zoneId = zoneData[i].id;
  566.                         break
  567.                     end
  568.                 end
  569.                 if id and zoneId then break end
  570.             end
  571.            
  572.             if not id and zoneId then
  573.                 print( "Could not find a boss by that name." );
  574.                 return;
  575.             end
  576.            
  577.             -- Pull reports in the last 30 days
  578.             local reportData = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/reports/guild/%s/%s/%s?api_key=%s&start=%s", config.guild, slugify( config.realm ), config.region, config.apiKey, ( os.time() - 30*60*60*24 ) * 1000 ) ) );
  579.            
  580.             local reports = {}
  581.            
  582.             for i=1,#reportData do
  583.                 if reportData[i].zone == zoneId then
  584.                     table.insert( reports, reportData[i].id );
  585.                 end
  586.             end
  587.            
  588.             local kills = {}
  589.            
  590.             -- Find all kills of that boss & difficulty
  591.             for i=1,#reports do
  592.                 local fightData = JSONDecode( MOD.HTTPSGet( string.format( "https://www.warcraftlogs.com/v1/report/fights/%s?api_key=%s", reports[i], config.apiKey ) ) );
  593.                
  594.                 for j=1,#fightData.fights do
  595.                     if fightData.fights[j].boss == id and fightData.fights[j].kill and fightData.fights[j].difficulty == difficulty then
  596.                         table.insert( kills, {
  597.                             ["time"] = fightData.fights[j].end_time - fightData.fights[j].start_time,
  598.                             ["fightId"] = fightData.fights[j].id,
  599.                             ["reportId"] = reports[i],
  600.                             ["date"] = fightData.fights[j].start_time,
  601.                         });
  602.                     end
  603.                 end
  604.             end
  605.            
  606.             -- Sort by kill time
  607.             table.sort( kills, function(a, b)
  608.                 return a.time < b.time;
  609.             end);
  610.            
  611.             if #kills > 0 then
  612.                 print( "Top " .. min( 3, #kills ) .. " kill times for " .. name .. ":" );
  613.                 for i=1,min( 3, #kills ) do
  614.                     local s = ""
  615.                     if i > 1 then
  616.                         s = s .. "| ";
  617.                     end
  618.                     print( s .. string.format( "%dm%02ds https://www.warcraftlogs.com/reports/%s#fight=%d", kills[i].time % 60, math.floor( kills[i].time / 60 ), kills[i].reportId, kills[i].fightId ) );
  619.                 end
  620.             end --]]
  621.         elseif UserStatus( user ) == 2 then
  622.             print( "bad input" );
  623.         end
  624.     end);
  625. end
Advertisement
Add Comment
Please, Sign In to add comment