<commit_before>
import ExtendableError from 'es6-error'
import invariant from 'invariant'

import needle from '../promised/needle'
import urls from '../constants/urls'

import mkcooldown from './cooldown'
import mklog from './log'
import {camelifyObject} from './format'

const cooldown = mkcooldown(130)
const log = mklog('api')
const logger = new mklog.Logger({sinks: {console: !!process.env.LET_ME_IN}})
const opts = {logger}

// cf. https://github.com/itchio/itchio-app/issues/48
// basically, lua returns empty-object instead of empty-array
// because they're the same in lua (empty table). not in JSON though.
export function ensureArray (v) {
  if (!v || ~~v.length === 0) {
    return []
  }
  return v
}

export class ApiError extends ExtendableError {
  constructor (errors) {
    super(errors.join(', '))
    this.errors = errors
  }

  toString () {
    return `API Error: ${this.errors.join(', ')}`
  }
}

/**
 async Wrapper for the itch.io API
 */
export class Client {
  constructor () {
    this.rootUrl = `${urls.itchioApi}/api/1`
    this.lastRequest = 0
  }

  async request (method, path, data = {}, transformers = {}) {
    const t1 = Date.now()

    const uri = `${this.rootUrl}${path}`

    await cooldown()
    const t2 = Date.now()

    const resp = await needle.requestAsync(method, uri, data)
    const body = resp.body
    const t3 = Date.now()

    const shortPath = path.replace(/^\/[^\/]*\//, '')
    log(opts, `${t2 - t1}ms wait, ${t3 - t2}ms http, ${method} ${shortPath} with ${JSON.stringify(data)}`)

    if (resp.statusCode !== 200) {
      throw new Error(`HTTP ${resp.statusCode}`)
    }

    if (body.errors) {
      throw new ApiError(body.errors)
    }
    const camelBody = camelifyObject(body)
    for (const key in transformers) {
      if (!transformers.hasOwnProperty(key)) continue
      camelBody[key] = transformers[key](camelBody[key])
    }

    return camelBody
  }

  async loginKey (key) {
    return await this.request('post', `/${key}/me`, {
      source: 'desktop'
    })
  }

  async loginWithPassword (username, password) {
    return await this.request('post', '/login', {
      username: username,
      password: password,
      source: 'desktop'
    })
  }

  withKey (key) {
    invariant(typeof key === 'string', 'API key is a string')
    return new AuthenticatedClient(this, key)
  }
}

export const client = new Client()
export default client

/**
 async A user, according to the itch.io API
 */
export class AuthenticatedClient {
  constructor (client, key) {
    this.client = client
    this.key = key
  }

  async request (method, path, data = {}, transformers = {}) {
    const url = `/${this.key}${path}`
    return await this.client.request(method, url, data, transformers)
  }

  // TODO: paging, for the prolific game dev.
  async myGames (data = {}) {
    return await this.request('get', `/my-games`, data, {games: ensureArray})
  }

  async myOwnedKeys (data = {}) {
    return await this.request('get', `/my-owned-keys`, data, {ownedKeys: ensureArray})
  }

  async me () {
    return await this.request('get', `/me`)
  }

  async myCollections () {
    return await this.request('get', `/my-collections`, {}, {collections: ensureArray})
  }

  async game (game) {
    return await this.request('get', `/game/${game}`)
  }

  async collection (collectionId) {
    return await this.request('get', `/collection/${collectionId}`)
  }

  async collectionGames (collectionId, page = 1) {
    return await this.request('get', `/collection/${collectionId}/games`, {page})
  }

  async search (query) {
    return await this.request('get', '/search/games', {query}, {games: ensureArray})
  }

  async downloadKeyUploads (downloadKeyId) {
    return await this.request('get', `/download-key/${downloadKeyId}/uploads`)
  }

  async downloadUploadWithKey (downloadKeyId, uploadId) {
    return await this.request('get', `/download-key/${downloadKeyId}/download/${uploadId}`)
  }

  async gameUploads (game) {
    return await this.request('get', `/game/${game}/uploads`, {}, {uploads: ensureArray})
  }

  async downloadUpload (uploadId) {
    return await this.request('get', `/upload/${uploadId}/download`)
  }
}<commit_msg>Ensure downloadKeyUploads returns an array of uploads<commit_after>
import ExtendableError from 'es6-error'
import invariant from 'invariant'

import needle from '../promised/needle'
import urls from '../constants/urls'

import mkcooldown from './cooldown'
import mklog from './log'
import {camelifyObject} from './format'

const cooldown = mkcooldown(130)
const log = mklog('api')
const logger = new mklog.Logger({sinks: {console: !!process.env.LET_ME_IN}})
const opts = {logger}

// cf. https://github.com/itchio/itchio-app/issues/48
// basically, lua returns empty-object instead of empty-array
// because they're the same in lua (empty table). not in JSON though.
export function ensureArray (v) {
  if (!v || ~~v.length === 0) {
    return []
  }
  return v
}

export class ApiError extends ExtendableError {
  constructor (errors) {
    super(errors.join(', '))
    this.errors = errors
  }

  toString () {
    return `API Error: ${this.errors.join(', ')}`
  }
}

/**
 async Wrapper for the itch.io API
 */
export class Client {
  constructor () {
    this.rootUrl = `${urls.itchioApi}/api/1`
    this.lastRequest = 0
  }

  async request (method, path, data = {}, transformers = {}) {
    const t1 = Date.now()

    const uri = `${this.rootUrl}${path}`

    await cooldown()
    const t2 = Date.now()

    const resp = await needle.requestAsync(method, uri, data)
    const body = resp.body
    const t3 = Date.now()

    const shortPath = path.replace(/^\/[^\/]*\//, '')
    log(opts, `${t2 - t1}ms wait, ${t3 - t2}ms http, ${method} ${shortPath} with ${JSON.stringify(data)}`)

    if (resp.statusCode !== 200) {
      throw new Error(`HTTP ${resp.statusCode}`)
    }

    if (body.errors) {
      throw new ApiError(body.errors)
    }
    const camelBody = camelifyObject(body)
    for (const key in transformers) {
      if (!transformers.hasOwnProperty(key)) continue
      camelBody[key] = transformers[key](camelBody[key])
    }

    return camelBody
  }

  async loginKey (key) {
    return await this.request('post', `/${key}/me`, {
      source: 'desktop'
    })
  }

  async loginWithPassword (username, password) {
    return await this.request('post', '/login', {
      username: username,
      password: password,
      source: 'desktop'
    })
  }

  withKey (key) {
    invariant(typeof key === 'string', 'API key is a string')
    return new AuthenticatedClient(this, key)
  }
}

export const client = new Client()
export default client

/**
 async A user, according to the itch.io API
 */
export class AuthenticatedClient {
  constructor (client, key) {
    this.client = client
    this.key = key
  }

  async request (method, path, data = {}, transformers = {}) {
    const url = `/${this.key}${path}`
    return await this.client.request(method, url, data, transformers)
  }

  // TODO: paging, for the prolific game dev.
  async myGames (data = {}) {
    return await this.request('get', `/my-games`, data, {games: ensureArray})
  }

  async myOwnedKeys (data = {}) {
    return await this.request('get', `/my-owned-keys`, data, {ownedKeys: ensureArray})
  }

  async me () {
    return await this.request('get', `/me`)
  }

  async myCollections () {
    return await this.request('get', `/my-collections`, {}, {collections: ensureArray})
  }

  async game (game) {
    return await this.request('get', `/game/${game}`)
  }

  async collection (collectionId) {
    return await this.request('get', `/collection/${collectionId}`)
  }

  async collectionGames (collectionId, page = 1) {
    return await this.request('get', `/collection/${collectionId}/games`, {page})
  }

  async search (query) {
    return await this.request('get', '/search/games', {query}, {games: ensureArray})
  }

  async downloadKeyUploads (downloadKeyId) {
    return await this.request('get', `/download-key/${downloadKeyId}/uploads`, {}, {uploads: ensureArray})
  }

  async downloadUploadWithKey (downloadKeyId, uploadId) {
    return await this.request('get', `/download-key/${downloadKeyId}/download/${uploadId}`)
  }

  async gameUploads (game) {
    return await this.request('get', `/game/${game}/uploads`, {}, {uploads: ensureArray})
  }

  async downloadUpload (uploadId) {
    return await this.request('get', `/upload/${uploadId}/download`)
  }
}
{'fraction_non_alphanumeric': 0.15705506972346964, 'fraction_numerical': 0.004727014890096904, 'mean_word_length': 3.3489208633093526, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 23, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4181509', 'n_tokens_mistral': 2762, 'n_tokens_neox': 2706, 'n_words': 726}