├── .gitignore ├── Dockerfile ├── config.json.example ├── package.json ├── readme.md └── ao-killbot.js /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | COPY . /usr/src/app 4 | 5 | WORKDIR /usr/src/app 6 | 7 | RUN npm install 8 | 9 | CMD ["npm","start"] 10 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "cmdPrefix": "!", 3 | "players": [ 4 | "Marn" 5 | ], 6 | "allianceName": "", 7 | "guildName": "8-bit", 8 | "username": "AO-Killbot", 9 | "admins": [ 10 | "" 11 | ], 12 | "botChannel": "", 13 | "playingGame": "Albion Killboard Bot", 14 | "token": "" 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ao-killbot", 3 | "version": "1.0.0", 4 | "description": "Albion Online killboard Discord bot", 5 | "main": "ao-killbot.js", 6 | "author": "Mark Arneman", 7 | "license": "ISC", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/bearlikelion/ao-killbot" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "start": "node ao-killbot.js" 15 | }, 16 | "dependencies": { 17 | "discord.js": "^11.5.1", 18 | "request": "^2.88.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AO-Killboard 2 | 3 | A discord bot for Albion Online's kill board 4 | 5 | ## Getting Started 6 | 7 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 8 | 9 | ![Screenshot](https://i.imgur.com/gLnvJpX.png) 10 | 11 | ### Usage 12 | 13 | * !ping - replies with @user pong 14 | * !kbclear - deletes all messages in the config.botChannel 15 | * !kbinfo - displays the kill board post for a specific kill to the current channel 16 | 17 | ### Prerequisites 18 | 19 | * [NodeJS](https://nodejs.org/) 20 | 21 | ### Installing 22 | 23 | * Install Node JS on the machine that will run the bot 24 | * Execute 'npm install' in the directory to download the dependencies 25 | * Create a new [Discord Application](https://discordapp.com/developers/applications/) 26 | * Copy config.json.example --> config.json 27 | * Add the 'APP BOT USER' token as 'token' in 'config.json' 28 | * Enable developer mode in Discord (Settings -> Appearance) 29 | * Right click the channel you wish to use as the kill board, and Copy ID 30 | * Set ID as 'botChannel' 31 | * OPTIONAL: Set User IDs for admin accounts 32 | 33 | * **To add the bot to your server**: Visit [https://discordapp.com/oauth2/authorize?client_id={YOUR CLIENT ID}](https://discordapp.com/oauth2/authorize?client_id=#) 34 | Example: [https://discordapp.com/api/oauth2/authorize?client_id=347919794504335362&permissions=2048&scope=bot](https://discordapp.com/oauth2/authorize?client_id=#) 35 | 36 | #### * to only display Guild kills and not alliance, set allianceName to something that cannot exist i.e 37 | 38 | ##### Example: config.json 39 | 40 | -- 41 | 42 | ```json 43 | { 44 | "cmdPrefix": "!", 45 | "allianceName": "", 46 | "guildName": "8-bit", 47 | "username": "AO-Killbot", 48 | "admins": [ 49 | "224865398034079430" 50 | ], 51 | "botChannel": "445822300890946337", 52 | "playingGame": "Albion Killboard Bot", 53 | "token": "zMznafHXfbgaD3k0.hYN.CDTzsMHXz_35MNMiGyLOT-8EoQotgEs10iZAa7" 54 | } 55 | 56 | ``` 57 | 58 | ### Built With 59 | 60 | * [Discord.js](https://github.com/hydrabolt/discord.js/) - Discord app library for Node.js and browsers. 61 | * [Request](https://github.com/request/request) - Simplified HTTP client 62 | 63 | ## Authors 64 | 65 | * **Mark Arneman** *Marn#8945* - [Arneman.me](http://arneman.me) 66 | -------------------------------------------------------------------------------- /ao-killbot.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Mark Arneman 3 | * @Date: 2017-08-18 11:12:18 4 | * @Last Modified by: Mark Arneman 5 | * @Last Modified time: 2019-06-11 6 | */ 7 | 8 | // Define static constants 9 | const config = require('./config.json'); 10 | 11 | // Require modules 12 | const Discord = require('discord.js'); 13 | const request = require('request'); 14 | 15 | if (typeof Discord !== 'undefined') { 16 | const client = new Discord.Client(); 17 | } 18 | 19 | var lastRecordedKill = -1; 20 | 21 | if (typeof config !== 'undefined') { 22 | // Convert all player names to lowercase 23 | var playerNames = []; 24 | for (var i = 0; i < config.players.length; i++) { 25 | playerNames.push(config.players[i].toLowerCase()) 26 | } 27 | } 28 | 29 | 30 | /** 31 | * Fetch recent kills from the Gameinfo API 32 | * @param {Number} limit [max kills to get] 33 | * @param {Number} offset [offset for first kill] 34 | * @return {json} [json array of events] 35 | */ 36 | function fetchKills(limit = 51, offset = 0) { 37 | request({ 38 | uri: 'https://gameinfo.albiononline.com/api/gameinfo/events?limit=' + limit + '&offset=' + offset, 39 | json: true 40 | }, function (error, response, body) { 41 | if (!error && response.statusCode === 200) { 42 | parseKills(body); 43 | } else { 44 | console.log('Error: ', error); // Log the error 45 | } 46 | }); 47 | } 48 | 49 | /** 50 | * Parse returned JSON from Gameinfo to 51 | * find alliance members on the killboard 52 | * @param {json} events 53 | */ 54 | function parseKills(events) { 55 | var count = 0; 56 | var breaker = lastRecordedKill; 57 | 58 | events.some(function (kill, index) { 59 | // Save the most recent kill for tracking 60 | if (index == 0) { 61 | lastRecordedKill = kill.EventId; 62 | } 63 | 64 | // Don't process data for the breaker KILL 65 | if (kill.EventId != breaker) { 66 | if (kill.Killer.AllianceName.toLowerCase() == config.allianceName.toLowerCase() || kill.Victim.AllianceName.toLowerCase() == config.allianceName.toLowerCase()) { 67 | // Alliance KILL 68 | postKill(kill); 69 | } else if (kill.Killer.GuildName.toLowerCase() == config.guildName.toLowerCase() || kill.Victim.GuildName.toLowerCase() == config.guildName.toLowerCase()) { 70 | // Guild Kill 71 | postKill(kill); 72 | } else if (playerNames.includes(kill.Killer.Name.toLowerCase()) || playerNames.includes(kill.Victim.Name.toLowerCase())) { 73 | // Player kill 74 | postKill(kill); 75 | } 76 | } else { 77 | count++; 78 | } 79 | 80 | return kill.EventId == breaker; 81 | }); 82 | 83 | // console.log('- Skipped ' + count + ' kills'); 84 | } 85 | 86 | function postKill(kill, channel = config.botChannel) { 87 | //quick fix to not post kills with 0 fame (like arena kills after the patch) 88 | if (kill.TotalVictimKillFame == 0) { 89 | return; 90 | } 91 | 92 | var victory = false; 93 | if (kill.Killer.AllianceName.toLowerCase() == config.allianceName.toLowerCase() || 94 | kill.Killer.GuildName.toLowerCase() == config.guildName.toLowerCase() || 95 | config.players.includes(kill.Killer.Name.toLowerCase())) { 96 | victory = true; 97 | } 98 | 99 | var assistedBy = ""; 100 | if (kill.numberOfParticipants == 1) { 101 | var soloKill = [ 102 | 'All on their own', 103 | 'Without assitance from anyone', 104 | 'All by himself', 105 | 'SOLO KILL' 106 | ]; 107 | assistedBy = soloKill[Math.floor(Math.random() * soloKill.length)]; 108 | } else { 109 | var assists = []; 110 | kill.Participants.forEach(function (participant) { 111 | if (participant.Name != kill.Killer.Name) { 112 | assists.push(participant.Name); 113 | } 114 | }) 115 | assistedBy = "Assisted By: " + assists.join(', '); 116 | } 117 | 118 | itemCount = 0; 119 | kill.Victim.Inventory.forEach(function (inventory) { 120 | if (inventory !== null) { 121 | itemCount++; 122 | } 123 | }); 124 | 125 | var itemsDestroyedText = ""; 126 | if (itemCount > 0) { 127 | itemsDestroyedText = " destroying " + itemCount + " items"; 128 | } 129 | 130 | var embed = { 131 | color: victory ? 0x008000 : 0x800000, 132 | author: { 133 | name: kill.Killer.Name + " killed " + kill.Victim.Name, 134 | icon_url: victory ? 'https://i.imgur.com/CeqX0CY.png' : 'https://albiononline.com/assets/images/killboard/kill__date.png', 135 | url: 'https://albiononline.com/en/killboard/kill/' + kill.EventId 136 | }, 137 | title: assistedBy + itemsDestroyedText, 138 | description: 'Gaining ' + kill.TotalVictimKillFame + ' fame', 139 | thumbnail: { 140 | url: (kill.Killer.Equipment.MainHand.Type ? 'https://gameinfo.albiononline.com/api/gameinfo/items/' + kill.Killer.Equipment.MainHand.Type + '.png' : 'https://albiononline.com/assets/images/killboard/kill__date.png') 141 | }, 142 | timestamp: kill.TimeStamp, 143 | fields: [{ 144 | name: "Killer Guild", 145 | value: (kill.Killer.AllianceName ? "[" + kill.Killer.AllianceName + "] " : '') + (kill.Killer.GuildName ? kill.Killer.GuildName : ''), 146 | inline: true 147 | }, 148 | { 149 | name: "Victim Guild", 150 | value: (kill.Victim.AllianceName ? "[" + kill.Victim.AllianceName + "] " : '') + (kill.Victim.GuildName ? kill.Victim.GuildName : ''), 151 | inline: true 152 | }, 153 | { 154 | name: "Killer IP", 155 | value: kill.Killer.AverageItemPower.toFixed(2), 156 | inline: true 157 | }, 158 | { 159 | name: "Victim IP", 160 | value: kill.Victim.AverageItemPower.toFixed(2), 161 | inline: true 162 | }, 163 | ], 164 | footer: { 165 | text: "Kill #" + kill.EventId 166 | } 167 | }; 168 | 169 | console.log(embed); 170 | 171 | client.channels.get(channel).send({ 172 | embed: embed 173 | }); 174 | } 175 | 176 | /** 177 | * Wait until ready and logged in 178 | * If we do not wait for the ready event 179 | * All commands will process before we are authorized 180 | */ 181 | if (typeof client !== 'undefined') { 182 | client.on('ready', () => { 183 | console.log('Ready and waiting!'); 184 | 185 | // If the config.username differs, change it 186 | if (client.user.username != config.username) { 187 | client.user.setUsername(config.username); 188 | } 189 | 190 | // Set 'Playing Game' in discord 191 | client.user.setActivity(config.playingGame); // broken due to discord API changes 192 | 193 | fetchKills(); 194 | 195 | // Fetch kills every 30s 196 | var timer = setInterval(function () { 197 | fetchKills(); 198 | }, 30000); 199 | }); 200 | } 201 | 202 | /** 203 | * On receive message 204 | */ 205 | if (typeof client !== 'undefined') { 206 | client.on('message', message => { 207 | if (message.content.indexOf(config.cmdPrefix) !== 0 || message.author.bot) return; 208 | else { // Execute command! 209 | var args = message.content.slice(config.cmdPrefix.length).trim().split(/ +/g); 210 | var command = args.shift().toLowerCase(); 211 | 212 | // Test Command - !ping 213 | if (command === 'ping') { 214 | message.reply('pong'); 215 | } else if (command === 'kbinfo') { 216 | request({ 217 | json: true, 218 | uri: 'https://gameinfo.albiononline.com/api/gameinfo/events/' + args[0] 219 | }, function (error, response, body) { 220 | if (!error && response.statusCode === 200) { 221 | postKill(body, message.channel.id); 222 | } else { 223 | console.log('Error: ', error); // Log the error 224 | } 225 | }); 226 | } 227 | 228 | // [ADMIN] - clear config.botChannel messages 229 | else if (command === 'kbclear') { 230 | if (config.admins.includes(message.author.id) && message.channel.id == config.botChannel) { 231 | message.channel.send('Clearing Killboard').then(msg => { 232 | msg.channel.fetchMessages().then(messages => { 233 | message.channel.bulkDelete(messages); 234 | console.log("[ADMIN] " + message.author.username + " cleared Killboard"); 235 | }) 236 | }) 237 | } 238 | } 239 | } 240 | }); 241 | } 242 | 243 | if (typeof config !== 'undefined') { 244 | if (config.token) { 245 | client.login(config.token); 246 | } else { 247 | console.log("ERROR: No bot token defined") 248 | } 249 | } else { 250 | console.log("ERROR: No config file") 251 | console.log("execute: cp config.json.example config.json") 252 | } --------------------------------------------------------------------------------