weverse
Version:
Wrapper for weverse private API
845 lines (844 loc) • 31.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WeverseClient = void 0;
const types_1 = require("../types");
const _1 = require(".");
const axios_1 = __importDefault(require("axios"));
const helpers_1 = require("./helpers");
const models_1 = require("../models");
const events_1 = __importDefault(require("events"));
/**
* WeverseEmitter allows the WeverseClient to emit events and provides methods for doing so
* @class
*/
class WeverseEmitter extends events_1.default {
constructor() {
super();
/**
* @param {Error} err - The error to be emitted
*/
this.newError = (err) => this.emit('error', err);
/**
* @param {boolean} initialized - whether initialization succeeded
*/
this.ready = (initialized) => this.emit('init', initialized);
/**
* @param {WeverseNotification} notification - new Notification to be emitted
*/
this.newNotif = (notification) => this.emit('notification', notification);
/**
* @param {WeversePost} post - new Post to be emitted
*/
this.newPost = (post) => this.emit('post', post);
/**
* @param {WeverseMedia} media - new Media to be emitted
*/
this.newMedia = (media) => this.emit('media', media);
/**
* @param {WeverseComment} comment - the Comment that was retrieved
* @param {WeversePost} post - the Post associated with the Comment
*/
this.newComment = (comment, post) => this.emit('comment', comment, post);
/**
* @param {boolean} result - boolean result of the login attempt
*/
this.loginResult = (result) => this.emit('login', result);
/**
* @param {boolean} status - result of the poll attempt. If true, Weverse was successfully polled
*/
this.polled = (status) => this.emit('poll', status);
}
}
/**
* Client for the private Weverse api
* @prop {WeverseCommunity[]} communities - The communities associated with the Weverse account
* @prop {WeverseArtist[]} artists - All artists in all communities associated with the account
* @prop {ClientNotifications} notifications - Subclass handling all notifications for the account
* @prop {WeversePost[]} posts - All posts that have been retrieved by this client
* @fires WeverseClient#error
* @fires WeverseClient#init
* @fires WeverseClient#notification
* @fires WeverseClient#post
* @fires WeverseClient#media
* @fires WeverseClient#comment
* @fires WeverseClient#login
* @fires WeverseClient#poll
* @class
*/
class WeverseClient extends WeverseEmitter {
/**
* @param {WeverseAuthorization} authorization - either {token: string} or {username: string, password: string}
* @param {boolean} verbose - optional; defaults to false
*/
constructor(authorization, verbose) {
super();
this.communities = [];
this._communityMap = new Map();
this.artists = [];
this._artistMap = new Map();
this.notifications = new models_1.ClientNotifications();
this.posts = [];
this._postsMap = new Map();
this._commentsMap = new Map();
if (authorization === undefined)
throw 'Must instantiate with Weverse token or login';
verbose = verbose !== null && verbose !== void 0 ? verbose : false;
this._authorized = false;
this._verbose = verbose;
this._authorization = authorization;
this._loginPayload = null;
this._credentials = null;
if ('token' in authorization) {
this._authType = 'token';
this._headers = { 'Authorization': 'Bearer ' + authorization.token };
}
else {
this._authType = 'password';
}
}
/**
* init options:
* allPosts: boolean - Whether to load all posts from each community into memory. This will be slow
* allNotifications: boolean - Whether to load all notifications for the Weverse account. Will be slow.
* allMedia: boolean - not currently implemented
* @param {WeverseInitOptions} options - optional
* @returns {Promise<void>}
* @public
*/
async init(options) {
try {
if (!await this.checkLogin())
return;
await this.getCommunities({ init: true });
if (!this.communities)
throw 'error';
this.log('Weverse: communities initialized');
await Promise.all(this.communities.map((c) => this.getCommunityArtists(c, { init: true })));
this.log('Weverse: artists initialized');
await this.getNotifications((options === null || options === void 0 ? void 0 : options.allNotifications) ? 0 : 1, true);
this.log('Weverse: notifications retreived');
await Promise.all(this.communities.map((c) => this.getCommunityPosts(c, (options === null || options === void 0 ? void 0 : options.allPosts) ? 0 : 1)));
this.log('Weverse: posts retreived');
this.ready(true);
}
catch (e) {
console.log('Weverse: initialization failed');
console.log(e);
this.ready(false);
}
}
/**
* Tells the client to start or stop listening for new notifications.
* Options:
* listen: boolean - Whether the client should be listening
* interval: boolean - Interval in MS to listen on
* process: boolean (optional) - Whether new notifications should be processed into Posts/Comments/Media
* @param {ListenOptions} opts
* @public
*/
listen(opts) {
var _a;
if (opts.listen === false) {
if (this.listener) {
clearInterval(this.listener);
this.log('Weverse: stopped listener');
}
}
else {
if (!opts.interval || opts.interval <= 0)
this.log('Weverse: set a positive interval');
else {
this.listener = setInterval(this.checker.bind(this, (_a = opts.process) !== null && _a !== void 0 ? _a : true), opts.interval);
this.log('Weverse: listening for new notifications');
}
}
}
/**
* Method passed to setInterval if client is listening for new notifications
* @param {boolean} process - Whether to process new notifications into Posts/Comments/Media
* @returns {Promise<void>}
* @protected
*/
async checker(process) {
try {
await this.getNewNotifications({ process });
this.polled(true);
}
catch (e) {
if (await this.checkLogin()) {
this.log('Weverse: successfully reconnected');
this.polled(true);
}
else {
this.log('Weverse: polling failed');
//this.newError(new Error('Weverse: failed to reconnect. Stopping listener.'))
this.polled(false);
}
}
}
/**
* Attempts to use a refresh token to get a new Weverse access token
* @returns {Promise<boolean>} Whether a new access token was granted
* @public
*/
async tryRefreshToken() {
if (!this._credentials)
return false;
try {
const refreshPayload = (0, helpers_1.createRefreshPayload)(this._credentials);
const response = await axios_1.default.post(_1.WeverseUrl.login, refreshPayload, { validateStatus: _1.validateStatus });
const credentials = response.data;
if ((0, types_1.isWeverseLogin)(credentials)) {
this._credentials = credentials;
this._headers = { Authorization: 'Bearer ' + credentials.access_token };
this._authorized = true;
this._refreshToken = credentials.refresh_token;
return true;
}
this.log('Weverse: token refresh rejected');
return false;
}
catch (_a) {
this.log('Weverse: error refreshing token');
return false;
}
}
/**
* Only used for password authentication. Attempts to login either with login given when
* the client was created, or with optional credentials parameter
* @param {WeversePasswordAuthorization} credentials - optional, will override initial credentials
* @returns {Promise<void>}
* @public
*/
async login(credentials) {
this._authorized = false;
if (credentials) {
this.log('Weverse: using provided credentials');
this._authorization = credentials;
this._authType = 'password';
}
try {
if (this._authType !== 'password') {
this.log('Weverse: provide credentials to call .login');
return;
}
this.createLoginPayload();
if (!this._loginPayload) {
this.log('Weverse: failed to generate login payload');
return;
}
const response = await axios_1.default.post(_1.WeverseUrl.login, this._loginPayload, { validateStatus: _1.validateStatus });
if (await this.handleResponse(response, _1.WeverseUrl.login)) {
const credentials = response.data;
if ((0, types_1.isWeverseLogin)(credentials)) {
this._credentials = credentials;
this._authorized = true;
this._authorization = { token: credentials.access_token };
this._authType = 'token';
this._headers = { 'Authorization': 'Bearer ' + credentials.access_token };
this._refreshToken = credentials.refresh_token;
this._weverseId = credentials.weMemberId;
this.log('Weverse password authorization succeeded');
}
else {
this.log('Weverse password authorization failed');
}
}
}
catch (e) {
this.log(e);
}
}
/**
* Force a credentials check. If login has already been converted to a token, token will be checked.
* @returns {Promise<boolean>} - whether the check was successful
* @public
*/
async checkLogin() {
if (this._authType === 'password') {
await this.login();
if (!this._authorized) {
console.log('Weverse: login failed. Check username + password, or provide a token instead');
return false;
}
else {
return true;
}
}
if (!await this.checkToken()) {
console.log('Weverse: invalid token / unable to refresh');
return false;
}
else {
return true;
}
}
/**
* Load all communities associated with this Weverse account. Returns the communities
* but also adds them to the cache.
* Options:
* init: boolean - Whether this method was called by the init method and should skip the login check
* @param {GetOptions} opts - optional
* @returns {Promise<WeverseCommunity[]>}
* @public
*/
async getCommunities(opts) {
if (!opts || !opts.init) {
if (!await this.checkLogin())
return null;
}
try {
const response = await axios_1.default.get(_1.WeverseUrl.communities, { headers: this._headers });
if (await this.handleResponse(response, _1.WeverseUrl.communities) && response.data.communities) {
const communities = (0, types_1.CommunityArray)(response.data.communities).map(_1.toCommunity);
communities.forEach((c) => {
this._communityMap.set(c.id, c);
});
this.communities = communities;
return communities;
}
return null;
}
catch (_a) {
this.log('Weverse: failed to get communities');
return null;
}
}
/**
* Get the artists in a community. Adds them to the cache and returns.
* Options:
* init - whether this method was called by init method and the login check should be skipped
* @param {WeverseCommunity} c
* @param {GetOptions} opts - optional
* @returns {Promise<WeverseArtist[] | null>} returns null if failed to fetch artists
*/
async getCommunityArtists(c, opts) {
if (!opts || !opts.init) {
if (!await this.checkLogin())
return null;
}
try {
const response = await axios_1.default.get(_1.WeverseUrl.community(c.id), { headers: this._headers });
if (await this.handleResponse(response, _1.WeverseUrl.community(c.id))) {
const data = response.data;
if (data.artists) {
const artists = (0, types_1.ArtistArray)(data.artists).map((a) => (0, _1.toArtist)(a, c));
c.addArtists(artists);
this.artists.push(...artists);
artists.forEach((a) => {
this._artistMap.set(a.id, a);
});
return artists;
}
}
this.log(`Weverse: failed to get artists for ${c.name}: bad response`);
return null;
}
catch (_a) {
this.log(`Weverse: error getting artists for ${c.name}`);
return null;
}
}
async getCommunityPosts(c, pages) {
let wvc;
if (typeof c === 'number') {
const temp = this._communityMap.get(c);
if (!temp) {
this.log('Weverse: community not found');
return null;
}
else {
wvc = temp;
}
}
else {
wvc = c;
}
if (pages === undefined)
pages = 1;
if (pages <= -1)
return null;
if (pages === 0)
pages = Infinity;
let count = 0;
let from = 0;
const posts = [];
while (count <= pages) {
try {
const response = await axios_1.default.get(_1.WeverseUrl.communityPostsPages(wvc.id, from), { headers: this._headers });
if (await this.handleResponse(response, _1.WeverseUrl.communityPostsPages(wvc.id, from))) {
const data = response.data;
if (data.posts) {
const newPosts = (0, types_1.PostArray)(data.posts).map((p) => {
const artist = this._artistMap.get(p.communityUser.artistId);
if (!artist) {
this.log('Weverse: failed to find artist for post:');
this.log(p);
return;
}
else {
return (0, _1.toPost)(p, wvc, artist);
}
}).filter(types_1.isPost);
const added = wvc.addPosts(newPosts);
await Promise.all(added.map(p => p.getVideoUrls(this._headers)));
this.posts.push(...added);
added.forEach((p) => {
this.newPost(p);
this._postsMap.set(p.id, p);
});
posts.push(...added);
}
if (typeof data.isEnded === 'boolean' && data.isEnded)
break;
from = data.lastId;
if (from == null || (typeof from === 'number' && from <= 0)) {
this.log('Weverse: malformed response from notifications endpoint');
break;
}
count++;
}
else {
throw new Error();
}
}
catch (_a) {
this.log('Weverse: failed to get notifications after ' + count + ' pages');
return posts;
}
}
return posts;
}
/**
* Note: If process = true, events will be emitted for new notifications AND new Posts/Comments/Media
* @param {number} pages - Optional number of pages to get; defaults to 1
* @param {boolean} process - Whether notifications should be processed into Posts/Comments/Media
* @returns {Promise<WeverseNotification[] | null>} - Returns only new notifications not already in cache, or null on failure
*/
async getNotifications(pages, process) {
if (pages === undefined)
pages = 1;
if (pages <= -1)
return null;
if (pages === 0)
pages = Infinity;
let count = 0;
let from = 0;
const notifications = [];
while (count < pages) {
try {
const response = await axios_1.default.get(_1.WeverseUrl.notifications(from), { headers: this._headers });
const { data } = response;
if (await this.handleResponse(response, _1.WeverseUrl.notifications(from))) {
const n = (0, types_1.NotificationArray)(data.notifications).map((n) => {
var _a;
if (n.communityId === 0)
return;
const artist = this._artistMap.get((_a = n.artistId) !== null && _a !== void 0 ? _a : -1);
const community = this._communityMap.get(n.communityId);
if (!community) {
this.log('Weverse: failed to find community for notification:');
this.log(n);
return;
}
else {
try {
return (0, _1.toNotification)(n, community, artist);
}
catch (e) {
this.log('malformed notification:');
this.log(n);
return;
}
}
}).filter(types_1.isNotification);
notifications.push(...this.notifications.addMany(n));
if (typeof data.isEnded === 'boolean' && data.isEnded)
break;
from = data.lastId;
if (from == null || (typeof from === 'number' && from <= 0)) {
this.log('Weverse: malformed response from notifications endpoint');
break;
}
count++;
}
else {
throw new Error();
}
}
catch (_a) {
this.log('Weverse: failed to get notifications after ' + count + ' pages');
break;
}
}
if (process) {
await Promise.all(notifications.map(async (n) => {
this.newNotif(n);
await this.processNotification(n);
}));
return notifications;
}
else {
return notifications;
}
}
/**
* Get one page of the most recent notifications
* @param {NewNotifications} opts - {process: boolean} - whether to process notifications into content
* @returns {Promise<WeverseNotification[] | null>}
*/
async getNewNotifications(opts) {
var _a;
if (!await this.checkLogin())
throw new Error();
return await this.getNotifications(1, (_a = opts === null || opts === void 0 ? void 0 : opts.process) !== null && _a !== void 0 ? _a : true);
}
/**
* Get a specific media object by id
* Will first check local cache, then request from Weverse
* @param {number} id
* @param {WeverseCommunity} community
* @returns {Promise<WeverseMedia | null>} - Returns only if media did not exist in cache
*/
async getMedia(id, community) {
const saved = community.mediaMap.get(id);
if (saved)
return saved;
try {
const response = await axios_1.default.get(_1.WeverseUrl.media(community.id, id), { headers: this._headers, validateStatus: _1.validateStatus });
if (await this.handleResponse(response, _1.WeverseUrl.media(community.id, id))) {
const data = response.data;
if (data.media) {
const media = (0, _1.toMedia)(data.media, community);
const added = community.addMedia(media);
if (added) {
this.newMedia(added);
return added;
}
else {
return null;
}
}
else {
throw new Error();
}
}
else {
throw new Error();
}
}
catch (_a) {
this.log(`Weverse: error getting media id ${id}`);
return null;
}
}
/**
* Gets all artist comments on a given post. Returns only new comments.
* @param {WeversePost} p
* @param {WeverseCommunity} c
* @param {number} cId?
* @returns {Promise<WeverseComment[] | null>}
*/
async getComments(p, c, cId) {
try {
const response = await axios_1.default.get(_1.WeverseUrl.postComments(p.id, c.id), { headers: this._headers });
if (await this.handleResponse(response, _1.WeverseUrl.postComments(p.id, c.id))) {
const data = response.data;
if (data.artistComments) {
const comments = (0, types_1.CommentArray)(data.artistComments).map((c) => {
const artist = this._artistMap.get(c.communityUser.artistId);
if (!artist)
return;
return (0, _1.toComment)(c, p, artist);
}).filter(types_1.isComment);
const added = p.addComments(comments);
added.forEach((c) => {
this._commentsMap.set(c.id, c);
this.newComment(c, p);
});
return added;
}
}
throw new Error();
}
catch (_a) {
this.log(`Weverse: error getting comments for post ${p.id}`);
return null;
}
}
/**
* Get one post by id. First checks the cache, then requests from Weverse.
* @param {number} id
* @param {number} communityId
* @returns {Promise<WeversePost | null>}
* @public
*/
async getPost(id, communityId) {
const saved = this._postsMap.get(id);
if (saved)
return saved;
try {
const response = await axios_1.default.get(_1.WeverseUrl.postDetails(id, communityId), { headers: this._headers });
if (await this.handleResponse(response, _1.WeverseUrl.postDetails(id, communityId))) {
const data = response.data;
if (data) {
const artistId = data.communityUser.artistId;
if (!artistId || typeof artistId !== 'number')
return null;
const community = this._communityMap.get(communityId);
const artist = this._artistMap.get(artistId);
if (!community || !artist)
return null;
const post = (0, _1.toPost)((0, types_1.Post)(data), community, artist);
await post.getVideoUrls(this._headers);
this.posts.push(post);
this._postsMap.set(post.id, post);
community.addPosts([post]);
this.newPost(post);
return post;
}
else {
return null;
}
}
throw new Error();
}
catch (e) {
this.log(`Weverse: error getting post ${id}`);
this.log(e);
return null;
}
}
/**
* Process one notification. If it refers to a post, comment, or media, attempt to add to cache
* @param {WeverseNotification} n
* @returns {Promise<void>}
*/
async processNotification(n) {
var _a, _b;
let k;
for (k in types_1.NotifContent) {
if (types_1.NotifContent[k].some(str => n.message.includes(str))) {
n.type = k;
break;
}
}
try {
switch (n.type) {
case types_1.NotifKeys.COMMENT:
const artist = this._artistMap.get((_a = n.artistId) !== null && _a !== void 0 ? _a : -1);
const replyTo = (_b = n.contentsExtraInfo) === null || _b === void 0 ? void 0 : _b.originContentId;
let postId;
let commentId = undefined;
if (typeof replyTo === 'number') {
postId = replyTo;
commentId = n.contentsExtraInfo.replyCommentId;
}
else {
postId = n.contentsId;
}
const post = await this.getPost(postId, n.community.id);
if (!post || !artist)
throw new Error();
await this.getComments(post, post.community, commentId);
break;
case types_1.NotifKeys.POST:
await this.getPost(n.contentsId, n.community.id);
break;
case types_1.NotifKeys.MEDIA:
await this.getMedia(n.contentsId, n.community);
break;
case types_1.NotifKeys.ANNOUNCEMENT:
break;
default:
this.log('Weverse: unknown notification type: ');
this.log(n);
}
}
catch (e) {
this.log(`failed to process notification ${n.id}: ${n.type}`);
}
}
/**
* Encrypt provided password with Weverse public RSA key and create payload to send to login endpoint
* Adds the payload as a property of the client, returns void
* @returns {void}
*/
createLoginPayload() {
try {
let payload = null;
if ((0, helpers_1.isWeversePasswordAuthorization)(this._authorization)) {
payload = (0, helpers_1.createLoginPayload)(this._authorization);
}
else {
return;
}
if (!payload)
return;
this._loginPayload = payload;
}
catch (e) {
return;
}
}
/**
* Check if the current token (provided or recieved from Weverse) is valid
* @returns {Promise<boolean>}
*/
async checkToken() {
if (this._authType !== 'token') {
this.log('Weverse: provide a token or call .login() with valid username + password');
return false;
}
try {
const response = await axios_1.default.get(_1.WeverseUrl.checkToken, { headers: this._headers, validateStatus: _1.validateStatus });
this.handleResponse(response, _1.WeverseUrl.checkToken);
if (response.status === 200) {
this._authorized = true;
return true;
}
else {
return this._authorized = await this.tryRefreshToken();
}
}
catch (e) {
this.log(`Weverse: failed to check token`);
this._authorized = false;
return false;
}
}
/**
* If the client receives a 401 unauthorized from Weverse, will attempt to refresh credentials
* @param {AxiosResponse} response
* @param {string} url
* @returns {Promise<boolean>}
*/
async handleResponse(response, url) {
if (response.status === 200)
return true;
if (response.status === 401) {
const refresh = await this.checkLogin();
if (!refresh) {
this.log('Weverse: failed to refresh token');
return false;
}
return true;
}
this.log(`Weverse: API error @ ${url}. Status code ${response.status}`);
return false;
}
/**
* Log something if verbose = true
* @param {any} ...vals
*/
log(...vals) {
if (this.verbose)
console.log(...vals);
}
/**
* Check the community hashmap for a given id
* @param {number} id
* @returns {WeverseCommunity | null}
*/
communityById(id) {
var _a;
return (_a = this._communityMap.get(id)) !== null && _a !== void 0 ? _a : null;
}
/**
* Check the artist hashmap for a given id
* @param {number} id
* @returns {WeverseArtist | null}
*/
artistById(id) {
var _a;
return (_a = this._artistMap.get(id)) !== null && _a !== void 0 ? _a : null;
}
/**
* Check the post hashmap for a given id
* @param {number} id
*/
post(id) {
var _a;
return (_a = this._postsMap.get(id)) !== null && _a !== void 0 ? _a : null;
}
get authorized() {
return this._authorized;
}
get credentials() {
return this._credentials;
}
get verbose() {
return this._verbose;
}
get authorization() {
return this._authorization;
}
get authType() {
return this._authType;
}
get loginPayload() {
return this._loginPayload;
}
get refreshToken() {
return this._refreshToken;
}
get weverseId() {
return this._weverseId;
}
get requestHeaders() {
return this._headers;
}
}
exports.WeverseClient = WeverseClient;
/**
* Error event
*
* @event WeverseClient#error
* @type {Error}
*/
/**
* Init event
* Whether initialization was successful
* @event WeverseClient#init
* @type {boolean}
*/
/**
* Notification event
* New notification
* @event WeverseClient#notification
* @type {WeverseNotification}
*/
/**
* Post event
* New post
* @event WeverseClient#post
* @type {WeversePost}
*/
/**
* Media event
* New media
* @event WeverseClient#media
* @type {WeverseMedia}
*/
/**
* Comment event
* New comment. Provides comment and post.
* @event WeverseClient#comment
* @type {WeverseComment}
* @type {WeversePost}
*/
/**
* Login event
* Result of login attempt.
* @event WeverseClient#login
* @type {boolean}
*/
/**
* Poll event
* Result of poll attempt.
* @event WeverseClient#poll
* @type {boolean}
*/