淘先锋技术网

首页 1 2 3 4 5 6 7

//Copyright 2008-2021 Jacky Zong . All rights reserved.
//  江山如此多娇,
//  引无数英雄竞折腰;
//  惜秦皇汉武
//  略输文采
import fs from 'fs';

import AxiosError from 'axios-error';
import FormData from 'form-data';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import invariant from 'ts-invariant';
import warning from 'warning';
import {
  OnRequestFunction,
  camelcaseKeys,
  createRequestInterceptor,
  onRequest,
  snakecaseKeys,
} from 'messaging-api-common';

import * as WechatTypes from './WechatTypes';

function throwErrorIfAny(response: AxiosResponse): AxiosResponse {
  const { errcode, errmsg } = response.data;
  if (!errcode || errcode === 0) return response;
  const msg = `WeChat API - ${errcode} ${errmsg}`;
  throw new AxiosError(msg, {
    response,
    config: response.config,
    request: response.request,
  });
}

export default class WechatClient {
 
  static connect(config: WechatTypes.ClientConfig): WechatClient {
    warning(
      false,
      '`WechatClient.connect(...)` is deprecated. Use `new WechatClient(...)` instead.'
    );
    return new WechatClient(config);
  }

 
  readonly axios: AxiosInstance;

 
  accessToken = '';

 
  private onRequest?: OnRequestFunction;

 
  private appId: string;

 
  private appSecret: string;

 
  private tokenExpiresAt = 0;

  constructor(config: WechatTypes.ClientConfig) {
    invariant(
      typeof config !== 'string',
      `WechatClient: do not allow constructing client with ${config} string. Use object instead.`
    );

    this.appId = config.appId;
    this.appSecret = config.appSecret;
    this.onRequest = config.onRequest || onRequest;
    const { origin } = config;

    this.axios = axios.create({
      baseURL: `${origin || 'https://api.weixin.qq.com'}/cgi-bin/`,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.axios.interceptors.request.use(
      createRequestInterceptor({
        onRequest: this.onRequest,
      })
    );
  }

  private async refreshToken(): Promise<void> {
    const { accessToken, expiresIn } = await this.getAccessToken();

    this.accessToken = accessToken;
    this.tokenExpiresAt = Date.now() + expiresIn * 1000;
  }

  private async refreshTokenWhenExpired(): Promise<void> {
    if (Date.now() > this.tokenExpiresAt) {
      await this.refreshToken();
    }
  }

 
  getAccessToken(): Promise<WechatTypes.AccessToken> {
    return this.axios
      .get<
        | { access_token: string; expires_in: number }
        | WechatTypes.FailedResponseData
      >(
        `/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`
      )
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

 

 
  async uploadMedia(
    type: WechatTypes.MediaType,
    media: Buffer | fs.ReadStream
  ): Promise<WechatTypes.UploadedMedia> {
    await this.refreshTokenWhenExpired();

    const form = new FormData();

    form.append('media', media);

    return this.axios
      .post<
        | { type: string; media_id: string; created_at: number }
        | WechatTypes.FailedResponseData
      >(`/media/upload?access_token=${this.accessToken}&type=${type}`, form, {
        headers: form.getHeaders(),
      })
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

 
  async getMedia(mediaId: string): Promise<WechatTypes.Media> {
    await this.refreshTokenWhenExpired();

    return this.axios
      .get<{ video_url: string } | WechatTypes.FailedResponseData>(
        `/media/get?access_token=${this.accessToken}&media_id=${mediaId}`
      )
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

 
  async sendRawBody(
    body: {
      touser: string;
    } & WechatTypes.SendMessageOptions &
      (
        | {
            msgtype: 'text';
            text: {
              content: string;
            };
          }
        | {
            msgtype: 'image';
            image: {
              mediaId: string;
            };
          }
        | {
            msgtype: 'voice';
            voice: {
              mediaId: string;
            };
          }
        | {
            msgtype: 'video';
            video: WechatTypes.Video;
          }
        | {
            msgtype: 'music';
            music: WechatTypes.Music;
          }
        | {
            msgtype: 'news';
            news: WechatTypes.News;
          }
        | {
            msgtype: 'mpnews';
            mpnews: {
              mediaId: string;
            };
          }
        | {
            msgtype: 'msgmenu';
            msgmenu: WechatTypes.MsgMenu;
          }
        | {
            msgtype: 'wxcard';
            wxcard: {
              cardId: string;
            };
          }
        | {
            msgtype: 'miniprogrampage';
            miniprogrampage: WechatTypes.MiniProgramPage;
          }
      )
  ): Promise<WechatTypes.SucceededResponseData> {
    await this.refreshTokenWhenExpired();

    return this.axios
      .post<WechatTypes.ResponseData>(
        `/message/custom/send?access_token=${this.accessToken}`,
        snakecaseKeys(body, { deep: true })
      )
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

 
  sendText(
    userId: string,
    text: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'text',
      text: {
        content: text,
      },
      ...options,
    });
  }

 
  sendImage(
    userId: string,
    mediaId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'image',
      image: {
        mediaId,
      },
      ...options,
    });
  }

 
  sendVoice(
    userId: string,
    mediaId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'voice',
      voice: {
        mediaId,
      },
      ...options,
    });
  }

 
  sendVideo(
    userId: string,
    video: WechatTypes.Video,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'video',
      video,
      ...options,
    });
  }

 
  sendMusic(
    userId: string,
    music: WechatTypes.Music,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'music',
      music,
      ...options,
    });
  }

 
  sendNews(
    userId: string,
    news: WechatTypes.News,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'news',
      news,
      ...options,
    });
  }

 
  sendMPNews(
    userId: string,
    mediaId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'mpnews',
      mpnews: {
        mediaId,
      },
      ...options,
    });
  }

 
  sendMsgMenu(
    userId: string,
    msgMenu: WechatTypes.MsgMenu,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'msgmenu',
      msgmenu: msgMenu,
      ...options,
    });
  }

 
  sendWXCard(
    userId: string,
    cardId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'wxcard',
      wxcard: {
        cardId,
      },
      ...options,
    });
  }

 
  sendMiniProgramPage(
    userId: string,
    miniProgramPage: WechatTypes.MiniProgramPage,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'miniprogrampage',
      miniprogrampage: miniProgramPage,
      ...options,
    });
  }

  // TODO: implement typing

  // TODO: 客服帳號相關
  
  }