<commit_before>import * as querystring from "querystring";

import { request, RequestFunc } from "../net";
import { camelifyObject } from "../format";
import urls from "../constants/urls";
import * as schemas from "./schemas";

import mkcooldown from "./cooldown";

import { contains } from "underscore";

import { IDownloadKey } from "../db/models/download-key";
import {
  ILoginKeyResult,
  IUpgradeResponse,
  IListUploadsResponse,
  ILoginWithPasswordResult,
  IMeResult,
  IMyGamesResult,
  IMyOwnedKeysResult,
  IMyCollectionsResult,
  IGameResult,
  IUserResult,
  ICollectionResult,
  ICollectionGamesResult,
  ISearchGamesResult,
  ISearchUsersResult,
  IDownloadUploadResult,
  IListBuildsResponse,
  IBuildResponse,
  BuildFileType,
  IDownloadBuildResult,
  IGameExtras,
  IListUploadsExtras,
  IPasswordOrSecret,
  IDownloadBuildFileExtras,
  ILoginExtras,
  ITotpVerifyResult,
} from "../types/api";

const DUMP_API_CALLS = process.env.LET_ME_IN === "1";
import { makeLogger, devNull } from "../logger";
import { normalize } from "idealizr";
const logger = DUMP_API_CALLS ? makeLogger({}) : devNull;

type HTTPMethod = "get" | "head" | "post";

interface ITransformerMap {
  [key: string]: (input: any) => any;
}

/**
 * async Wrapper for the itch.io API
 */
export class Client {
  /** base URL for API requests */
  rootUrl: string;

  /** timestamp of last request */
  lastRequest: number;

  /** http request maker */
  requestFunc: RequestFunc;

  cooldown = mkcooldown(130);

  constructor() {
    this.rootUrl = `${urls.itchioApi}/api/1`;
    this.lastRequest = 0;
    this.requestFunc = request;
  }

  /**
   * Make a `method` http request to `path`, passing `data` as parameters (GET, POST body, etc.)
   * 
   * The body is:
   *   - transformed is transformers are passed
   *   - normalized if a schema is passed
   *   - camelified
   */
  async request(params: IRequestParams): Promise<any> {
    const { method, data, path } = params;

    const t1 = Date.now();

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

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

    const resp = await this.requestFunc(method, uri, data, {});
    let body = resp.body;
    const t3 = Date.now();

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

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

    if (body.errors) {
      throw new ApiError(body.errors);
    }

    const { transformers } = params;
    if (transformers) {
      for (const key in transformers) {
        if (!transformers.hasOwnProperty(key)) {
          continue;
        }
        body[key] = transformers[key](body[key]);
      }
    }

    const { schema } = params;
    if (schema) {
      body = normalize(body, schema);
    }

    body = camelifyObject(body);

    return body;
  }

  /**
   * Log in using an API key
   */
  async loginKey(key: string): Promise<ILoginKeyResult> {
    return await this.request({
      method: "get",
      path: `/${key}/me`,
      data: {
        source: "desktop",
      },
    });
  }

  async loginWithPassword(
    username: string,
    password: string,
    extras: ILoginExtras
  ): Promise<ILoginWithPasswordResult> {
    let data: {
      username: string;
      password: string;
      source: "desktop";
      recaptcha_response?: string;
      v: number;
    } = {
      username: username,
      password: password,
      source: "desktop",
      v: 3,
    };
    if (data.recaptcha_response) {
      data.recaptcha_response = extras.recaptchaResponse;
    }

    return await this.request({ method: "post", path: "/login", data });
  }

  async totpVerify(token: string, code: string): Promise<ITotpVerifyResult> {
    const data = { token, code };
    return await this.request("post", "/totp/verify", data);
  }

  withKey(key: string): AuthenticatedClient {
    return new AuthenticatedClient(this, key);
  }

  hasAPIError(errorObject: ApiError, apiError: string): boolean {
    return contains(errorObject.errors || [], apiError);
  }
}

export const client = new Client();
export default client;

interface IRequestParams {
  method: HTTPMethod;
  path: string;
  data?: any;
  // TODO: remove when {}-instead-of-[] is fixed server-side
  transformers?: ITransformerMap;
  schema?: any;
}

/**
 * A user, according to the itch.io API
 */
export class AuthenticatedClient {<commit_msg>fix totpVerify API post-rebase<commit_after>import * as querystring from "querystring";

import { request, RequestFunc } from "../net";
import { camelifyObject } from "../format";
import urls from "../constants/urls";
import * as schemas from "./schemas";

import mkcooldown from "./cooldown";

import { contains } from "underscore";

import { IDownloadKey } from "../db/models/download-key";
import {
  ILoginKeyResult,
  IUpgradeResponse,
  IListUploadsResponse,
  ILoginWithPasswordResult,
  IMeResult,
  IMyGamesResult,
  IMyOwnedKeysResult,
  IMyCollectionsResult,
  IGameResult,
  IUserResult,
  ICollectionResult,
  ICollectionGamesResult,
  ISearchGamesResult,
  ISearchUsersResult,
  IDownloadUploadResult,
  IListBuildsResponse,
  IBuildResponse,
  BuildFileType,
  IDownloadBuildResult,
  IGameExtras,
  IListUploadsExtras,
  IPasswordOrSecret,
  IDownloadBuildFileExtras,
  ILoginExtras,
  ITotpVerifyResult,
} from "../types/api";

const DUMP_API_CALLS = process.env.LET_ME_IN === "1";
import { makeLogger, devNull } from "../logger";
import { normalize } from "idealizr";
const logger = DUMP_API_CALLS ? makeLogger({}) : devNull;

type HTTPMethod = "get" | "head" | "post";

interface ITransformerMap {
  [key: string]: (input: any) => any;
}

/**
 * async Wrapper for the itch.io API
 */
export class Client {
  /** base URL for API requests */
  rootUrl: string;

  /** timestamp of last request */
  lastRequest: number;

  /** http request maker */
  requestFunc: RequestFunc;

  cooldown = mkcooldown(130);

  constructor() {
    this.rootUrl = `${urls.itchioApi}/api/1`;
    this.lastRequest = 0;
    this.requestFunc = request;
  }

  /**
   * Make a `method` http request to `path`, passing `data` as parameters (GET, POST body, etc.)
   * 
   * The body is:
   *   - transformed is transformers are passed
   *   - normalized if a schema is passed
   *   - camelified
   */
  async request(params: IRequestParams): Promise<any> {
    const { method, data, path } = params;

    const t1 = Date.now();

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

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

    const resp = await this.requestFunc(method, uri, data, {});
    let body = resp.body;
    const t3 = Date.now();

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

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

    if (body.errors) {
      throw new ApiError(body.errors);
    }

    const { transformers } = params;
    if (transformers) {
      for (const key in transformers) {
        if (!transformers.hasOwnProperty(key)) {
          continue;
        }
        body[key] = transformers[key](body[key]);
      }
    }

    const { schema } = params;
    if (schema) {
      body = normalize(body, schema);
    }

    body = camelifyObject(body);

    return body;
  }

  /**
   * Log in using an API key
   */
  async loginKey(key: string): Promise<ILoginKeyResult> {
    return await this.request({
      method: "get",
      path: `/${key}/me`,
      data: {
        source: "desktop",
      },
    });
  }

  async loginWithPassword(
    username: string,
    password: string,
    extras: ILoginExtras
  ): Promise<ILoginWithPasswordResult> {
    let data: {
      username: string;
      password: string;
      source: "desktop";
      recaptcha_response?: string;
      v: number;
    } = {
      username: username,
      password: password,
      source: "desktop",
      v: 3,
    };
    if (data.recaptcha_response) {
      data.recaptcha_response = extras.recaptchaResponse;
    }

    return await this.request({ method: "post", path: "/login", data });
  }

  async totpVerify(token: string, code: string): Promise<ITotpVerifyResult> {
    const data = { token, code };
    return await this.request({ method: "post", path: "/totp/verify", data });
  }

  withKey(key: string): AuthenticatedClient {
    return new AuthenticatedClient(this, key);
  }

  hasAPIError(errorObject: ApiError, apiError: string): boolean {
    return contains(errorObject.errors || [], apiError);
  }
}

export const client = new Client();
export default client;

interface IRequestParams {
  method: HTTPMethod;
  path: string;
  data?: any;
  // TODO: remove when {}-instead-of-[] is fixed server-side
  transformers?: ITransformerMap;
  schema?: any;
}

/**
 * A user, according to the itch.io API
 */
export class AuthenticatedClient {
{'fraction_non_alphanumeric': 0.13548671590783817, 'fraction_numerical': 0.003748208576783155, 'mean_word_length': 3.033792796798577, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6296343', 'n_tokens_mistral': 2990, 'n_tokens_neox': 2938, 'n_words': 752}