import { TreeNode } from './providers/BaseProvider' ; import { AccountRestrictedError , LoginRequiredError } from './error' ; import * as cheerio from 'cheerio' ; import * as template from 'art-template' ; import http from './http' ; import { AxiosResponse } from 'axios' ; import * as path from 'path' ; import G from './global' ; import * as FormData from 'form-data' ; import topicItemClick from './commands/topicItemClick' ; export class V2ex { param tab static async getTopicListByTab ( tab ) { const { data : html } = await http . get ( `https://www.v2ex.com/?tab=${ tab }` ) ; const $ = cheerio . load ( html ) ; const cells = $ ( '#Main > .box' ) . eq ( 0 ) . children ( '.cell.item' ) ; const list = [ ] ; cells . each ( ( _ , cell ) => { const topicElement = $ ( cell ) . find ( 'a.topic-link' ) ; const nodeElement = $ ( cell ) . find ( 'a.node' ) ; const topic = new Topic ( ) ; topic . title = topicElement . text ( ) . trim ( ) ; topic . link = 'https://www.v2ex.com' + topicElement . attr ( 'href' ) ?. split ( '#' ) [ 0 ] ; topic . node = { name : nodeElement . attr ( 'href' ) ?. split ( 'go/' ) [ 1 ] || '' , title : nodeElement . text ( ) . trim ( ) , } ; list . push ( topic ) ; } ) ; return list ; } param node static async getTopicListByNode ( node ) { const { data : html } = await http . get ( `https://www.v2ex.com/go/${ node . name }` ) ; const $ = cheerio . load ( html ) ; const cells = $ ( '#TopicsNode .cell[class*="t_"]' ) ; const list = [ ] ; cells . each ( ( _ , cell ) => { const topicElement = $ ( cell ) . find ( 'a.topic-link' ) ; const topic = new Topic ( ) ; topic . title = topicElement . text ( ) . trim ( ) ; topic . link = 'https://www.v2ex.com' + topicElement . attr ( 'href' ) ?. split ( '#' ) [ 0 ] ; topic . node = node ; list . push ( topic ) ; } ) ; return list ; } param topicLink static async getTopicDetail ( topicLink ) { const res = await http . get < string > ( topicLink + '?p=1' ) ; const $ = cheerio . load ( res . data ) ; /**
         * 部分帖子需要登录查看
         * 第1种：会重定向到登录页（https://www.v2ex.com/signin?next=/t/xxxxxx），并提示：你要查看的页面需要先登录。如交易区：https://www.v2ex.com/t/704753
         * 第2种：会重定向到首页，无提示。如：https://www.v2ex.com/t/704716
         * 第3种：账号访问受限（如新用户），会重定向到 https://www.v2ex.com/restricted
         */ if ( res . request . _redirectable . _redirectCount > 0 ) { if ( res . request . path . indexOf ( '/signin' ) >= 0 ) { G . setCookie ( '' ) ; throw new LoginRequiredError ( '你要查看的页面需要先登录' ) ; } if ( res . request . path === '/' ) { if ( G . getCookie ( ) ) { throw new Error ( '您无权访问此页面' ) ; } else { throw new LoginRequiredError ( '你要查看的页面需要先登录' ) ; } } if ( res . request . path . indexOf ( '/restricted' ) === 0 ) { throw new AccountRestrictedError ( '访问受限，详情请查看 <a href="https://www.v2ex.com/restricted">https://www.v2ex.com/restricted</a>' ) ; } throw new Error ( '未知错误' ) ; } const topic = new TopicDetail ( ) ; topic . id = parseInt ( topicLink . split ( '/t/' ) [ 1 ] || '0' ) ; topic . link = topicLink ; topic . once = $ ( 'a.light-toggle' ) . attr ( 'href' ) ?. split ( '?once=' ) [ 1 ] || '' ; topic . title = $ ( '.header > h1' ) . text ( ) ; const node = $ ( '.header > a' ) . eq ( 1 ) ; topic . node = { name : node . attr ( 'href' ) ?. split ( 'go/' ) [ 1 ] || '' , title : node . text ( ) . trim ( ) , } ; topic . authorAvatar = $ ( '.header > .fr img.avatar' ) . attr ( 'src' ) || '' ; const meta = $ ( '.header > .gray' ) . text ( ) . split ( '·' ) ; topic . authorName = meta [ 0 ] . trim ( ) ; topic . displayTime = meta [ 1 ] . trim ( ) ; topic . visitCount = parseInt ( meta [ 2 ] . trim ( ) ) ; topic . content = $ ( '.topic_content' ) . html ( ) || '' ; $ ( '.subtle' ) . each ( ( _ , element ) => { topic . appends . push ( { time : $ ( element ) . children ( '.fade' ) . text ( ) . split ( '·' ) [ 1 ] . trim ( ) , content : $ ( element ) . children ( '.topic_content' ) . html ( ) || '' , } ) ; } ) ; const topicButtons = $ ( '.topic_buttons' ) ; if ( topicButtons . length ) { const countStr = topicButtons . children ( '.topic_stats' ) . text ( ) ; if ( /(\d+)\s*人收藏/ . test ( countStr ) ) { topic . collectCount = parseInt ( RegExp . $1 ) ; } if ( /(\d+)\s*人感谢/ . test ( countStr ) ) { topic . thankCount = parseInt ( RegExp . $1 ) ; } const collectButton = topicButtons . children ( 'a.tb' ) . eq ( 0 ) ; topic . isCollected = collectButton . text ( ) . indexOf ( '取消收藏' ) >= 0 ; topic . collectParamT = collectButton . attr ( 'href' ) ?. split ( '?t=' ) [ 1 ] || null ; topic . canThank = topicButtons . children ( '#topic_thank' ) . length > 0 ; topic . isThanked = topicButtons . find ( '.topic_thanked' ) . length > 0 ; } topic . replyCount = parseInt ( $ ( '#Main > .box' ) . eq ( 1 ) . children ( 'div.cell' ) . eq ( 0 ) . find ( 'span.gray' ) . text ( ) . split ( '•' ) [ 0 ] ) || 0 ; param $ const _getTopicReplies = ( $ ) : TopicReply [ ] => { const replies = [ ] ; $ ( '#Main > .box' ) . eq ( 1 ) . children ( 'div[id].cell' ) . each ( ( _ , element ) => { replies . push ( { userAvatar : $ ( element ) . find ( 'img.avatar' ) . attr ( 'src' ) || '' , userName : $ ( element ) . find ( 'a.dark' ) . html ( ) || '' , time : $ ( element ) . find ( 'span.ago' ) . text ( ) , floor : $ ( element ) . find ( 'span.no' ) . text ( ) , content : $ ( element ) . find ( '.reply_content' ) . html ( ) || '' , thanks : parseInt ( $ ( element ) . find ( 'span.small.fade' ) . text ( ) . trim ( ) || '0' ) , } ) ; } ) ; return replies ; } ; topic . replies = _getTopicReplies ( $ ) ; const pager = $ ( '#Main > .box' ) . eq ( 1 ) . find ( '.cell:not([id]) table' ) ; if ( pager . length ) { const totalPage = parseInt ( pager . find ( 'td' ) . eq ( 0 ) . children ( 'a' ) . last ( ) . text ( ) ) ; console . log ( `${ topicLink }：一共${ totalPage }页回复` ) ; const promises = [ ] ; for ( let p = 2 ; p <= totalPage ; p ++ ) { promises . push ( http . get < string > ( topicLink + `?p=${ p }` ) ) ; } try { const resList = await Promise . all ( promises ) ; resList . map ( ( res ) => res . data ) . forEach ( ( html ) => { const replies = _getTopicReplies ( cheerio . load ( html ) ) ; topic . replies = topic . replies . concat ( replies ) ; if ( topic . replies . length > topic . replyCount ) { topic . replyCount = topic . replies . length ; } } ) ; } catch ( error ) { } } return topic ; } param topicLink param content param once static async postReply ( topicLink , content , once ) { const form = new FormData ( ) ; form . append ( 'content' , content ) ; form . append ( 'once' , once ) ; await http . post ( topicLink , form , { headers : form . getHeaders ( ) , } ) ; } param cookie static async checkCookie ( cookie ) { if ( ! cookie ) { return false ; } const res = await http . get ( 'https://www.v2ex.com/t' , { headers : { Cookie : cookie , } , } ) ; return res . request . _redirectable . _redirectCount <= 0 ; } private static _cachedNodes : Node [ ] = [ ] ; /**
     * 获取所有节点
     */ static async getAllNodes ( ) { if ( this . _cachedNodes . length ) { return this . _cachedNodes ; } const { data : html } = await http . get < string > ( 'https://www.v2ex.com/planes' ) ; const $ = cheerio . load ( html ) ; const nodes = [ ] ; $ ( 'a.item_node' ) . each ( ( _ , element ) => { nodes . push ( { name : $ ( element ) . attr ( 'href' ) ?. split ( 'go/' ) [ 1 ] || '' , title : $ ( element ) . text ( ) . trim ( ) , } ) ; } ) ; console . log ( `获取到${ nodes . length }个节点` ) ; this . _cachedNodes = nodes ; return nodes ; } /**
     * 获取我收藏的节点
     */ static async getCollectionNodes ( ) { const res = await http . get < string > ( 'https://www.v2ex.com/my/nodes' ) ; if ( res . request . _redirectable . _redirectCount > 0 ) { G . setCookie ( '' ) ; throw new LoginRequiredError ( '你要查看的页面需要先登录' ) ; } const $ = cheerio . load ( res . data ) ; const nodes = [ ] ; $ ( '#my-nodes > a.fav-node' ) . each ( ( _ , element ) => { nodes . push ( { name : $ ( element ) . attr ( 'href' ) ?. split ( 'go/' ) [ 1 ] || '' , title : $ ( element ) . children ( '.fav-node-name' ) . text ( ) . trim ( ) . split ( ' ' ) [ 0 ] , } ) ; } ) ; return nodes ; } returns { Promise < DailyRes > } static async daily ( ) { const { data : html } = await http . get < string > ( '/mission/daily' ) ; const $ = cheerio . load ( html ) ; if ( $ ( '.fa-ok-sign' ) . length ) { return DailyRes . repetitive ; } const btn = $ ( 'input.super.normal.button' ) ; if ( btn . length ) { if ( /once=(\d+)/ . test ( btn . attr ( 'onclick' ) || '' ) ) { const once = RegExp . $1 ; const { data : html2 } = await http . get < string > ( `/mission/daily/redeem?once=${ once }` ) ; const $2 = cheerio . load ( html2 ) ; if ( $2 ( '.fa-ok-sign' ) . length ) { return DailyRes . success ; } } } return DailyRes . failed ; } param topicId param t static async collectTopic ( topicId , t ) { await http . get < string > ( `/favorite/topic/${ topicId }?t=${ t }` ) ; } param topicId param t static async cancelCollectTopic ( topicId , t ) { await http . get < string > ( `/unfavorite/topic/${ topicId }?t=${ t }` ) ; } param topicId param once static async thankTopic ( topicId , once ) { const { data : res } = await http . post ( `/thank/topic/${ topicId }?once=${ once }` ) ; return ! ! res . success ; } param q param from param size static async search ( q , from = 0 , size = 10 ) { const { data : res } = await http . get ( 'https://www.sov2ex.com/api/search' , { params : { q , from , size , } , } ) ; const hits = res . hits || [ ] ; return hits . map ( ( h ) => h . _source ) ; } param page param data static renderPage ( page , data = { } ) { const templatePath = path . join ( G . context ! . extensionPath , 'html' , page ) ; const html = template ( templatePath , data ) ; return html ; } param context static openTestPage ( ) { const item = new TreeNode ( '写了一个 VSCode 上可以逛 V2EX 的插件' , false ) ; item . link = 'https://www.v2ex.com/t/703733' ; topicItemClick ( item ) ; } } /**
 * 话题
 */ export class Topic { /** 标题 */ public title : string = '' ; /** 链接 */ public link : string = '' ; /** 节点 */ public node : Node = { title : '' , name : '' } ; } /**
 * 话题详情
 */ export class TopicDetail { /** id */ public id : number = 0 ; /** 链接 */ public link : string = '' ; /** 校验参数，可用来判断是否登录或登录是否有效 */ public once : string = '' ; /** 标题 */ public title : string = '' ; /** 节点 */ public node : Node = { title : '' , name : '' } ; /** 作者头像 */ public authorAvatar : string = '' ; /** 作者名字 */ public authorName : string = '' ; /** 时间 */ public displayTime : string = '' ; /** 点击次数 */ public visitCount : number = 0 ; /** 内容 */ public content : string = '' ; /** 追加内容 */ public appends : TopicAppend [ ] = [ ] ; /** 收藏人数 */ public collectCount : number = 0 ; /** 感谢人数 */ public thankCount : number = 0 ; /** 是否已收藏 */ public isCollected : boolean = false ; /** 是否已感谢 */ public isThanked : boolean = false ; /** 是否能发送感谢（自己的帖子不能发送感谢） */ public canThank : boolean = true ; /** 收藏/取消收藏参数t */ public collectParamT : string | null = null ; /** 回复总条数 */ public replyCount : number = 0 ; /** 回复 */ public replies : TopicReply [ ] = [ ] ; } /**
 * 话题追加内容
 */ export class TopicAppend { /** 追加时间 */ public time : String = '' ; /** 追加内容 */ public content : string = '' ; } /**
 * 话题回复
 */ export class TopicReply { /** 用户头像 */ public userAvatar : string = '' ; /** 用户名 */ public userName : string = '' ; /** 回复时间 */ public time : string = '' ; /** 楼层 */ public floor : string = '' ; /** 回复内容 */ public content : string = '' ; /** 感谢数 ❤ */ public thanks : number = 0 ; } /**
 * 节点
 */ export class Node { /** 节点名称 */ public name : string = '' ; /** 节点标题（显示的名称） */ public title : string = '' ; } /**
 * 签到结果
 */ export enum DailyRes { /**签到成功 */ success = '签到成功' , /**重复签到 */ repetitive = '重复签到' , /**签到失败 */ failed = '签到失败' } /**
 * sov2ex搜索结果的source字段
 */ export class SoV2exSource { /**帖子id */ public id : number = 0 ; /**发帖人 */ public member : string = '' ; /**帖子标题 */ public title : string = '' ; /**帖子内容 */ public content : string = '' ; /**回复数量 */ public replies : number = 0 ; /**发帖时间 */ public created : string = '' ; }