Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
kindred-api
Advanced tools
Kindred is a thin Node.js wrapper on top of Riot Games API for League of Legends
Kindred is a thin Node.js wrapper (with an optional rate limiter) on top of Riot Games API for League of Legends.
Hopefully there aren't too many bugs! I'm currently focusing on refactoring the code now.
My goal is to make a wrapper that is simple, sensible, and consistent. This project is heavily inspired by psuedonym117's Python wrapper. Look at the Usage Section to see what I mean.
This is my first try at making an API wrapper. I am open to any advice and help!
March 28, 2017
I'm pretty proud of the end result. The rate limiting isn't the best, but it does work and is enforced per region! Also, the method names are kinda iffy (minor inconsistencies), but it works really well for my other project and for when I need a quick script. I'll try to improve this library over the next few weeks.
yarn add kindred-api
// or npm install kindred-api
Make sure to check the official Riot Documentation to see what query parameters you can pass in to each endpoint (through the options parameter)!
Note: All region
parameters are OPTIONAL. All options
parameters are OPTIONAL unless stated otherwise.
k.Champion.getAll({ region: REGIONS.KOREA }, rprint)
k.Champion.get({ championID: 67 }, rprint)
k.Champion.get({ championID: 67 }).then(data => console.log(data))
k.Champion.get({ championID: 67, region: 'kr' }, rprint)
k.ChampionMastery.get({ playerID: 20026563, championID: 203 }, rprint)
k.ChampionMastery.get({ playerID: 20026563, championID: 203 }).then(data => console.log(data))
k.ChampionMastery.getChampionMasteries({ id: 20026563 }, rprint)
k.ChampionMastery.getAll({ id: 20026563 }).then(data => console.log(data))
k.ChampionMastery.totalScore({ id: 20026563 }, rprint)
k.ChampionMastery.best({ id: 20026563 }, rprint)
k.CurrentGame.get({ name: 'Contractz' }, rprint)
k.FeaturedGames.getFeaturedGames(rprint)
k.FeaturedGames.get().then(data => console.log(data))
k.FeaturedGames.get({ region: 'na' }, rprint)
k.Game.get({ summonerID: 20026563 }, rprint)
k.League.getLeagues({ summonerID: 20026563 }, rprint)
k.League.get({ summonerID: 20026563 }, rprint)
k.League.getEntries({ summonerID: 20026563 }, rprint)
k.League.challengers(rprint)
k.League.challengers({ region: 'na' }, rprint)
k.League.masters().then(data => console.log(data))
k.Status.get().then(data => console.log(data))
k.Status.all().then(data => console.log(data))
k.Match.get({ id: 1912829920 }, rprint)
k.MatchList.get({ id: 20026563 }, rprint)
k.RunesMasteries.runes({ id: 20026563 }, rprint)
k.RunesMasteries.masteries({ id: 20026563 }, rprint)
k.Stats.ranked({ id: 20026563 }, rprint)
k.Stats.summary({ id: 20026563 }, rprint)
k.Summoner.get({ name: 'caaaaaaaaarIa' }, rprint)
k.Summoner.all({ names: ['caaaaaaaaarIa', 'Contractz'] }, rprint)
k.Summoner.get({ id: 20026563 }, rprint)
k.Summoner.getAll({ ids: [20026563, 32932398] }, rprint)
k.Summoner.names({ ids: [20026563, 32932398] }, rprint)
var KindredAPI = require('kindred-api')
// var RIOT_API_KEY = require('whatever')
// or if you're using something like dotenv..
require('dotenv').config()
var RIOT_API_KEY = process.env.RIOT_API_KEY
var REGIONS = KindredAPI.REGIONS
var LIMITS = KindredAPI.LIMITS
var CACHE_TYPES = KindredAPI.CACHE_TYPES
/*
Default region for every method call is NA,
but you can set it during initialization as shown below.
You can also change it with 'setRegion(region)' as well.
To NOT use the built-in rate limiter, do NOT pass in anything
into limits.
*/
var k = new KindredAPI.Kindred({
key: RIOT_API_KEY,
defaultRegion: REGIONS.NORTH_AMERICA,
debug: true, // shows status code, urls, and relevant headers
limits: [ [10, 10], [500, 600] ], // user key
// 10 requests per 10 seconds, 500 requests per 10 minutes
// You can just pass in LIMITS.DEV, LIMITS.PROD, 'dev', or 'prod' instead though.
cacheOptions: CACHE_TYPES[0] // in memory
})
console.log(CACHE_TYPES)
// ['in-memory-cache', 'redis']
function rprint(err, data) { console.log(data) }
/*
NOTE: Making any form of parameter error will inform you
what parameters you can pass in!
*/
k.getSummoner(rprint)
// getSummoners request FAILED; required params `ids` (array of ints), `id` (int), `names` (array of strings), or `name` (string) not passed in
k.Summoner.get(rprint)
// same as above
k.getSummoner(rprint)
// getSummoner request FAILED; required params `id` (int) or `name` (string) not passed in
k.getTopChamps(rprint)
// getTopChamps request FAILED; required params `id` (int) or `playerID` (int) not passed in
k.getChampMastery(rprint)
// getChampMastery request FAILED; required params `playerID` (int) AND `championID` (int) not passed in
k.ChampionMastery.get(rprint)
// same as above
/*
Notice the OR and the AND!!
Note: getChampMastery is the only method that can't take in an 'id' parameter,
because it requires both a 'playerID' and a 'championID'!
*/
/*
The first parameter of all endpoint methods will ALWAYS be an object.
However, when the parameters are satisfied by default parameters and/or
only have optional parameters, you can simply pass your callback in.
*/
k.getChallengers(rprint) // default region, default solo queue mode, valid
k.League.challengers(rprint) // same as above
k.getRuneList(rprint) // only optional arguments & not passing in any optional arguments, valid
k.Static.runes(rprint)
/*
I have recently added namespacing to the methods.
You can check out all the namespaces and the aliases in the code
since I didn't mention them in the endpoints-covered
section yet.
All the namespaces are named after the official names Riot have given them.
*/
k.League.getChallengers(rprint)
k.League.challengers(rprint)
k.League.challengers()
.then(data => console.log(data))
.catch(err => console.error(err))
/*
getSummoners & getSummoner target both the by-name and by-id endpoints.
In the case of the summoner endpoints, it made a lot more sense for the two
functions to target both the by-name and by-id summoner endpoints.
The example above targets the by-name endpoint, while
the example below targets the by-id endpoint.
*/
k.getSummoner({ id: 354959 }, rprint)
k.Summoner.getSummoner({ id: 354959 }, rprint)
k.Summoner.get({ id: 354959 }, rprint)
k.Summoner.get({ id: 354959 })
.then(data => console.log(data))
.catch(err => console.error(err))
/*
The 'id', 'ids', 'name', and 'names' parameters
stay consistent throughout the API but for the one
exception above. However, I do have aliases for them.
For example, for summoners, you have summonerID, summonerIDs,
playerID, and playerIDs.
Plural parameters can take in both arrays and singular values.
Single parameters, however, can only take singular values.
*/
k.getSummoner({ summonerID: 354959 }, rprint)
k.getSummoner({ summonerID: 354959 })
.then(json => console.log(json))
.catch(err => console.log(err))
k.getSummoners({ summonerIDs: [354959, 21542029] }, rprint)
k.getMatch({ id: 2459973154 }, rprint)
k.getMatch({ matchID: 2459973154 }, rprint)
k.Match.get({ id: 2459973154 }, rprint)
k.Match.get({ id: 2459973154 })
.then(data => console.log(data))
.catch(err => console.error(err))
var names = ['beautifulkorean', 'c9gun', 'caaaaaaaaarIa']
k.Summoner.getAll({ names }, rprint) // getSummoners
var ids = [22059766, 20026563, 44989337]
k.Summoner.names({ ids }, rprint)
k.getSummoners({ names: 'caaaaaaaaaria' }, rprint)
k.getSummoners({ name: 'caaaaaaaaaria' }, rprint)
/* Every method has an optional 'region' parameter. */
var options = { name: 'sktt1peanut', region: REGIONS.KOREA }
k.getSummoner(options, rprint) // peanut's data
/* Changing the default region! */
k.setRegion(REGIONS.KOREA)
/* Note that you can use spaces in the name. */
var fakerIgn = { name: 'hide on bush' }
var fakerId
k.getSummoner(fakerIgn, function (err, data) {
/*
But you should sanitize the name if you want to access the dictionary.
{ hideonbush:
{ id: 4460427,
name: 'Hide on bush',
profileIconId: 6,
revisionDate: 1490355284000,
summonerLevel: 30 } }
*/
fakerId = data[fakerIgn.name.replace(/\s/g, '').toLowerCase()].id
console.log('fakerId:', fakerId)
}) // faker's data
/*
Note that the player runes endpoint only accepts
a comma-separated list of integers.
*/
k.setRegion(REGIONS.NORTH_AMERICA)
k.getRunes({ ids: [354959, 21542029] }, rprint)
k.getRunes({ id: 354959 }, rprint)
k.getRunes({ ids: 354959 }, rprint)
k.getRunes({ id: 354959 })
.then(json => console.log(json))
.catch(err => console.error(err))
k.Runes.get({ id: 354959 })
.then(json => console.log(json))
.catch(err => console.log(err))
/*
But what if you want to quickly get the rune pages given
that you have a list of names?
You'd chain it like in many other clients:
Get the ids from the names, get the runes from the ids.
*/
names = ['Richelle', 'Grigne']
k.getSummoners({ names }, function (err, data) {
var ids = []
for (var name of names)
ids.push(data[name.replace(/\s/g, '').toLowerCase()].id)
k.getRunes({ ids }, rprint)
})
/* I find that inconvenient, and so I just chain it for you in my code. */
// all methods that target endpoints that only accept ids
k.getRunes({ names: ['Richelle', 'Grigne'] }, rprint)
k.getRunes({ name: 'Richelle' }, rprint)
k.getRecentGames({ name: 'Richelle' }, rprint)
k.getLeagues({ names: ['Richelle', 'Grigne'] }, rprint)
k.getCurrentGame({ name: 'Fràe', region: REGIONS.OCEANIA }, rprint)
k.getLeagues({ names: ['Richelle', 'Grigne'] })
.then(data => console.log(data))
var name = 'Grigne'
k.RunesMasteries.runes({ name })
.then(data => console.log(data))
k.Runes.get({ name })
.then(data => console.log(data))
k.Masteries.get({ name })
.then(data => console.log(data))
/*
Functions will have an options parameter that you can pass in query
strings when applicable. Values of options should match the
endpoint's 'Query Parameters'. Check the methods to see which methods
you can pass in options to.
Some are required, and some are not. I often take care of the ones
that are required by using the most sensible defaults.
For example, the required parameter for many methods is 'type' (of queue).
I made it so that the default is 'RANKED_SOLO_5x5' if 'type' is not passed
in.
*/
k.getChallengers({ region: 'na' }, rprint) // get challengers from ranked solo queue ladder
k.getChallengers({ region: 'na', options: {
type: 'RANKED_FLEX_SR'
}}, rprint) // get challengers from ranked flex ladder
k.Match.get({ id: 2459973154 }, rprint) // includes timeline by default
k.Match.get({ id: 2459973154, options: { includeTimeline: false } }, rprint)
/*
However, for getMatchList, the endpoint uses an optional
'rankedQueues' instead of 'type' to allow multiple options.
I still set the default to RANKED_SOLO_5x5 though.
*/
var name = 'caaaaaaaaaria'
k.getSummoners({ region: 'na', names: name }, function (err, data) {
if (data) {
k.getMatchList({ region: 'na', id: data[name].id, options: {
/*
According to Riot API, query parameters that can accept multiple values
must be a comma separated list (or a single value), which is why I do the below 'join'.
You can also simply do 'RANKED_SOLO_5x5,RANKED_FLEX_SR'.
*/
rankedQueues: ['RANKED_SOLO_5x5', 'RANKED_FLEX_SR'].join(','),
championIds: '67' // '267,67' or ['267', '67'].join(',')
} }, rprint)
}
})
/* The above example with promises. */
var name = 'caaaaaaaaaria'
var opts = {
region: 'na',
options: {
rankedQueues: ['RANKED_SOLO_5x5', 'RANKED_FLEX_SR'].join(','),
championIDs: '67'
}
}
k.getSummoner({ name, region: opts.region })
.then(data => k.getMatchList(
Object.assign({ id: data[name].id }, opts)
))
.then(data => console.log(data))
.catch(err => console.err(error))
var furyMasteryId = 6111
k.getMastery({ id: furyMasteryId }, rprint)
k.Static.getMastery({ id: furyMasteryId }, rprint)
var msRuneId = 10002
k.Static.getRune({ id: msRuneId }, rprint)
April 2 I have added caching support. Right now, the library supports in-memory caching as well as caching with redis. These are the default timers that made sense to me.
const endpointCacheTimers = {
// defaults
CHAMPION: cacheTimers.MONTH,
CHAMPION_MASTERY: cacheTimers.SIX_HOURS,
CURRENT_GAME: cacheTimers.NONE,
FEATURED_GAMES: cacheTimers.NONE,
GAME: cacheTimers.HOUR,
LEAGUE: cacheTimers.SIX_HOURS,
STATIC: cacheTimers.MONTH,
STATUS: cacheTimers.NONE,
MATCH: cacheTimers.MONTH,
MATCH_LIST: cacheTimers.ONE_HOUR,
RUNES_MASTERIES: cacheTimers.HOUR,
STATS: cacheTimers.HOUR,
SUMMONER: cacheTimers.DAY
}
If you pass in cacheOptions, but not how long you want each type of request to be cached (cacheTTL object), then by default you'll use the above timers.
To pass in your own custom timers, initialize Kindred like this:
import TIME_CONSTANTS from KindredAPI.TIME_CONSTANTS // for convenience, has a bunch of set timers in seconds
var k = new KindredAPI.Kindred({
key: RIOT_API_KEY,
defaultRegion: REGIONS.NORTH_AMERICA,
debug: true, // you can see if you're retrieving from cache with lack of requests showing
limits: [ [10, 10], [500, 600] ],
cacheOptions: CACHE_TYPES[0] // in-memory
cacheTTL :{
// All values in SECONDS.
CHAMPION: whatever,
CHAMPION_MASTERY: whatever,
CURRENT_GAME: whatever,
FEATURED_GAMES: whatever,
GAME: whatever,
LEAGUE: whatever,
STATIC: TIME_CONSTANTS.MONTH,
STATUS: whatever,
MATCH: whatever,
MATCH_LIST: whatever,
RUNES_MASTERIES: whatever,
STATS: whatever,
SUMMONER: TIME_CONSTANTS.DAY
}
})
Feel free to make a PR regarding anything (even the smallest typo or inconsistency).
There are a few inconsistencies and weird things within this libary that I don't know how to address since this is my first API wrapper and I'm still quite a big newbie.
For example, the two methods getChamp() and getChampion() are actually different.
getChamp() targets the champ endpoint
getChampion() targets the static endpoint
I didn't want to attach getChampion() with 'static' in any way or form since I thought it looked kind of annoying because then I would want to attach static to the other static methods as well (maybe that's better?).
March 31: I decided to combat the above by just namespacing the functions (k.Static.getChampion vs k.Champion.getChampion/get). The original functions are still usable though.
Right now, the code is also quite messy and there is a lot of repeated code. Function definitions are quite long because I include many aliases as well. I haven't thought of an elegant way to make a magic function that manages to work for every single endpoint request yet.
Any help and/or advice is appreciated!
FAQs
Node.js League of Legends v3 API wrapper with built-in rate-limiting (enforced per region, burst/spread, follows retry headers, app/method rate-limiting), caching (in-memory, Redis), automatic retries, and parameter checking.
The npm package kindred-api receives a total of 51 weekly downloads. As such, kindred-api popularity was classified as not popular.
We found that kindred-api demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.