├── capture.png ├── capture ├── a.png ├── b.png └── c.png ├── README.md └── v2ex.sh /capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/six-ddc/v2ex-shell-client/HEAD/capture.png -------------------------------------------------------------------------------- /capture/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/six-ddc/v2ex-shell-client/HEAD/capture/a.png -------------------------------------------------------------------------------- /capture/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/six-ddc/v2ex-shell-client/HEAD/capture/b.png -------------------------------------------------------------------------------- /capture/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/six-ddc/v2ex-shell-client/HEAD/capture/c.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## shell版v2ex客户端 2 | 3 | ### Install: 4 | 5 | 基本使用依赖需安装jq用于命令行解析json,另外签到功能还需额外安装httpie: 6 | 7 | OS X: 8 | 9 | ``` 10 | brew install jq 11 | brew install httpie 12 | ``` 13 | 14 | Ubuntu: 15 | 16 | ``` 17 | sudo apt-get install jq 18 | sudo apt-get install httpie 19 | ``` 20 | 21 | 其他平台按照可参见 [jq官方文档](https://stedolan.github.io/jq/download/),[httpie官方文档](https://github.com/jkbrzt/httpie) 22 | 23 | ### Usage: 24 | 25 | ``` 26 | ➜ v2ex-shell-client git:(master) ✗ ./v2ex.sh 27 | NONE # help 28 | --------------------------------------------------------------------------------------------------------------- 29 | hot | 热门主题 30 | late | 最新主题 31 | login/relogin | 登录/重新登录 32 | daily | 领取每日签到奖励 33 | cate | 获取指定分类的主题 34 | node | 获取节点的主题 35 | | 查看指定主题序号的所有回复 36 | open | 使用默认浏览器查看指定主题贴 37 | help | 查看帮助 38 | q|quit | 退出 39 | --------------------------------------------------------------------------------------------------------------- 40 | ``` 41 | 42 | ![image](https://raw.githubusercontent.com/six-ddc/v2ex-shell-client/master/capture/a.png) 43 | 44 | 主题的回复内容将单独显示,而不影响主题列表 45 | 46 | ![image](https://raw.githubusercontent.com/six-ddc/v2ex-shell-client/master/capture/b.png) 47 | ![image](https://raw.githubusercontent.com/six-ddc/v2ex-shell-client/master/capture/c.png) 48 | 49 | ### TODO: 50 | 51 | * 翻页支持 52 | * ~~登录支持~~ 53 | * 回复功能 54 | * 常用节点选择 55 | * 排版优化 56 | -------------------------------------------------------------------------------- /v2ex.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # set -xe 4 | 5 | reset="\e[0m" 6 | 7 | red="\e[0;31m" 8 | green="\e[0;32m" 9 | yellow="\e[0;33m" 10 | blue="\e[0;34m" 11 | pink="\e[0;35m" 12 | cyan="\e[0;36m" 13 | 14 | RED="\e[1;31m" 15 | GREEN="\e[1;32m" 16 | YELLOW="\e[1;33m" 17 | BLUE="\e[1;34m" 18 | PINK="\e[1;35m" 19 | CYAN="\e[1;36m" 20 | 21 | red_back="\e[0;41m" 22 | green_back="\e[0;42m" 23 | yellow_back="\e[0;43m" 24 | blue_back="\e[0;44m" 25 | pink_back="\e[0;45m" 26 | cyan_back="\e[0;46m" 27 | 28 | RET="" 29 | HTTP="" 30 | 31 | USER_NAME="" 32 | MODE="none" 33 | ARRAY=() 34 | RRAY_TITLE=() 35 | ARRAY_CONTENT=() 36 | ARRAY_URL=() 37 | 38 | TEMP_FILE="/tmp/ddc.v2ex.shell.tmp" 39 | COOKIE_FILE="cookie.jar" 40 | LOGIN_STATE=0 41 | 42 | _topics() { 43 | if [ "$1" = "topics" ]; then 44 | if test "$HTTP"; then 45 | $HTTP "https://www.v2ex.com/api/topics/$2.json" > $TEMP_FILE 46 | else 47 | curl -s -o $TEMP_FILE "https://www.v2ex.com/api/topics/$2.json" 48 | fi 49 | elif [ "$1" = "node" ]; then 50 | if test "$HTTP"; then 51 | $HTTP -f GET "https://www.v2ex.com/api/topics/show.json?node_name=$2" > $TEMP_FILE 52 | else 53 | curl -s -o $TEMP_FILE "https://www.v2ex.com/api/topics/show.json?node_name=$2" 54 | fi 55 | else 56 | return 57 | fi 58 | if [ "$(cat $TEMP_FILE | jq -r '. | type')" != "array" ]; then 59 | printf "${red}节点名不存在,另外节点名称不支持中文,如酷工作请使用jobs${reset}\n" 60 | return 61 | fi 62 | LENGTH=$(cat $TEMP_FILE | jq ". | length") 63 | if ! test $LENGTH; then 64 | return 65 | fi 66 | ARRAY=() 67 | ARRAY_TITLE=() 68 | ARRAY_CONTENT=() 69 | ARRAY_URL=() 70 | echo $TEMP_FILE 71 | for((i = 0; i < $LENGTH; i++)) 72 | do 73 | # 替换百分号可能引起的printf输出异常,只能在jq后解析,而不能单独通过echo解析 74 | title=$(jq -r ".[$i].title" $TEMP_FILE | sed "s/\%/\%\%/g") 75 | content=$(jq -r ".[$i].content" $TEMP_FILE | sed "s/\%/\%\%/g") 76 | url=$(jq -r ".[$i].url" $TEMP_FILE) 77 | member=$(jq -r ".[$i].member.username" $TEMP_FILE) 78 | node_title=$(jq -r ".[$i].node.title" $TEMP_FILE | sed "s/\%/\%\%/g") 79 | replies=$(jq -r ".[$i].replies" $TEMP_FILE) 80 | title="$blue$node_title$reset $green$title$reset $pink$member$reset($cyan$replies$reset)" 81 | id=$(jq -r ".[$i].id" $TEMP_FILE) 82 | ARRAY[$(($i+1))]=$id 83 | ARRAY_TITLE[$(($i+1))]="$title" 84 | ARRAY_CONTENT[$(($i+1))]="$content" 85 | ARRAY_URL[$(($i+1))]="$url" 86 | printf "%2d. $title\n" "$(($i+1))" 87 | done 88 | # echo ${ARRAY[@]} 89 | } 90 | 91 | _date() { 92 | if [ $(uname) = "Darwin" ]; then 93 | if [ $(date +%Y) -eq $(date -r $1 +%Y) ]; then 94 | if [ $(date +%m) -eq $(date -r $1 +%m) ] && [ $(date +%d) -eq $(date -r $1 +%d) ]; then 95 | RET=$(date -r $1 +"%H:%M:%S") 96 | else 97 | RET=$(date -r $1 +"%m-%d %H:%M:%S") 98 | fi 99 | else 100 | RET=$(date -r $1 +"%Y-%m-%d %H:%M:%S") 101 | fi 102 | else 103 | if [ $(date +%Y) -eq $(date -d @$1 +%Y) ]; then 104 | if [ $(date +%m) -eq $(date -d @$1 +%m) ] && [ $(date +%d) -eq $(date -d @$1 +%d) ]; then 105 | RET=$(date -d @$1 +"%H:%M:%S") 106 | else 107 | RET=$(date -d @$1 +"%m-%d %H:%M:%S") 108 | fi 109 | else 110 | RET=$(date -d @$1 +"%Y-%m-%d %H:%M:%S") 111 | fi 112 | fi 113 | } 114 | 115 | _categories() { 116 | if test "$HTTP"; then 117 | # $HTTP --session v2ex -f GET "https://www.v2ex.com/?tab=$1" > $TEMP_FILE 118 | curl -s -b $COOKIE_FILE -o $TEMP_FILE "https://www.v2ex.com/?tab=$1" 119 | else 120 | curl -s -b $COOKIE_FILE -o $TEMP_FILE "https://www.v2ex.com/?tab=$1" 121 | fi 122 | # grep的文件和重定向不能是同一个文件 123 | grep '' $TEMP_FILE > $TEMP_FILE.grep 124 | reg_id=' $TEMP_FILE 137 | else 138 | curl -s -o $TEMP_FILE "https://www.v2ex.com/api/topics/show.json?id=$id" 139 | fi 140 | title=$(jq -r ".[0].title" $TEMP_FILE | sed "s/\%/\%\%/g") 141 | content=$(jq -r ".[0].content" $TEMP_FILE | sed "s/\%/\%\%/g") 142 | url=$(jq -r ".[0].url" $TEMP_FILE) 143 | member=$(jq -r ".[0].member.username" $TEMP_FILE) 144 | node_title=$(jq -r ".[0].node.title" $TEMP_FILE | sed "s/\%/\%\%/g") 145 | replies=$(jq -r ".[0].replies" $TEMP_FILE) 146 | title="$blue$node_title$reset $green$title$reset $pink$member$reset($cyan$replies$reset)" 147 | ARRAY[$i]=$id 148 | ARRAY_TITLE[$i]="$title" 149 | ARRAY_CONTENT[$i]="$content" 150 | ARRAY_URL[$i]="$url" 151 | printf "%2d. $title\n" "$i" 152 | i=$(($i+1)) 153 | if [ $i -gt 10 ]; then 154 | break 155 | fi 156 | fi 157 | done < $TEMP_FILE.grep 158 | } 159 | 160 | _replies() { 161 | id=${ARRAY[$1]} 162 | if ! test $id; then 163 | printf "${red}列表序列号越界${reset}\n" 164 | return 165 | fi 166 | replies_tmpfile="$TEMP_FILE.less" 167 | printf "${ARRAY_TITLE[$1]} ${ARRAY_URL[$1]}\n${green}${ARRAY_CONTENT[$1]}${reset}\n" > $replies_tmpfile 168 | if test "$HTTP"; then 169 | $HTTP -f GET "https://www.v2ex.com/api/replies/show.json?topic_id=$id" > $TEMP_FILE 170 | else 171 | curl -s -o $TEMP_FILE "https://www.v2ex.com/api/replies/show.json?topic_id=$id" 172 | fi 173 | LENGTH=$(cat $TEMP_FILE | jq ". | length") 174 | if ! test $LENGTH; then 175 | return 176 | fi 177 | for((i = 0; i < $LENGTH; i++)) 178 | do 179 | content=$(jq -r ".[$i].content" $TEMP_FILE | sed "s/\%/\%\%/g") 180 | member=$(jq -r ".[$i].member.username" $TEMP_FILE) 181 | created=$(jq -r ".[$i].created" $TEMP_FILE) 182 | thanks=$(jq -r ".[$i].thanks" $TEMP_FILE) 183 | _date $created 184 | created=$RET 185 | id=$(jq -r ".[$i].member.id" $TEMP_FILE) 186 | if [[ $thanks != "0" && $thanks != "null" ]]; then 187 | printf "\n${blue}%3dL${reset}. $pink$member$reset $cyan$created$reset ♥️ $RED$thanks$reset\n${green}$content${reset}\n" "$(($i+1))" >> $replies_tmpfile 188 | else 189 | printf "\n${blue}%3dL${reset}. $pink$member$reset $cyan$created$reset\n${green}$content${reset}\n" "$(($i+1))" >> $replies_tmpfile 190 | fi 191 | done 192 | # 只有加上-r选项,多行文本的ascii color才会被当作一行处理显示,但是却有回滚时颜色不连续的异常,属于less的bug,暂不能解决。 193 | less -rCm $replies_tmpfile 194 | } 195 | 196 | _sel() { 197 | if [ ${#ARRAY[@]} -gt 0 ]; then 198 | _replies $1 199 | else 200 | printf "${red}请先获取主题列表${reset}\n" 201 | _usage 202 | fi 203 | } 204 | 205 | _open() { 206 | if [ ${#ARRAY[@]} -gt 0 ]; then 207 | id=${ARRAY[$1]} 208 | if [ -z $id ]; then 209 | printf "${red}列表序列号越界${reset}\n" 210 | return 211 | fi 212 | url="https://www.v2ex.com/t/$id" 213 | printf "${green}$url${reset}\n" 214 | if [ $(uname) = "Darwin" ]; then 215 | open "$url" 216 | else 217 | # 暂不清楚桌面环境linux是否可行 218 | if [ -n $BROWSER ]; then 219 | $BROWSER "$url" 220 | elif which xdg-open >/dev/null 2>&1; then 221 | xdg-open "$url" 222 | elif which gnome-open >/dev/null 2>&1; then 223 | gnome-open "$url" 224 | fi 225 | fi 226 | else 227 | printf "${red}请先获取主题列表${reset}\n" 228 | _usage 229 | fi 230 | } 231 | 232 | _login() { 233 | v2ex_sign='https://www.v2ex.com/signin' 234 | if test "$HTTP"; then 235 | if test "$1"; then 236 | USER_NAME="" 237 | rm -f ~/.httpie/sessions/www.v2ex.com/v2ex.json > /dev/null 238 | fi 239 | $HTTP --session v2ex $v2ex_sign > $TEMP_FILE 240 | else 241 | if test "$1"; then 242 | curl -s -o $TEMP_FILE -c $COOKIE_FILE $v2ex_sign 243 | else 244 | curl -s -o $TEMP_FILE -c $COOKIE_FILE -b $COOKIE_FILE $v2ex_sign 245 | fi 246 | fi 247 | USER_KEY=$(grep -oE 'type="text".*[a-z0-9]{64}' $TEMP_FILE | grep -oE '[a-z0-9]{64}') 248 | PASS_KEY=$(grep -oE 'type="password".*[a-z0-9]{64}' $TEMP_FILE | grep -oE '[a-z0-9]{64}') 249 | # printf "$USER_KEY,$PASS_KEY\n" 250 | grep '登出' $TEMP_FILE > /dev/null 251 | if [ $? != 0 ]; then 252 | once=$(grep 'name="once"' $TEMP_FILE) 253 | reg_once='value="([0-9]+)" name="once"' 254 | if [[ $once =~ $reg_once ]]; then 255 | once=${BASH_REMATCH[1]} 256 | printf "username: " 257 | read username 258 | printf "password: " 259 | read -s password 260 | printf "\n" 261 | if test "$HTTP"; then 262 | $HTTP --session v2ex -f POST $v2ex_sign $USER_KEY=${username} $PASS_KEY=${password} once=${once} next=/ Referer:$v2ex_sign > $TEMP_FILE 263 | else 264 | curl -s -o $TEMP_FILE -c $COOKIE_FILE -b $COOKIE_FILE -d "$USER_KEY=${username}&$PASS_KEY=${password}&once=${once}&next=/" -e "$v2ex_sign" $v2ex_sign 265 | fi 266 | grep '用户名和密码无法匹配' $TEMP_FILE > /dev/null 267 | if [ $? -eq 0 ]; then 268 | printf "${red}用户名或密码错误...${reset}\n" 269 | return 1 270 | fi 271 | else 272 | printf "${red}登录异常...${reset}\n" 273 | return 1 274 | fi 275 | fi 276 | # printf "获取用户信息...\n" 277 | if test "$HTTP"; then 278 | $HTTP --session v2ex https://www.v2ex.com/ > $TEMP_FILE 279 | else 280 | curl -s -o $TEMP_FILE -b $COOKIE_FILE https://www.v2ex.com/ 281 | fi 282 | user_info=$(grep "bigger.*member" $TEMP_FILE) 283 | reg_user='member.*>(.*)' 284 | if [[ $user_info =~ $reg_user ]]; then 285 | USER_NAME="${BASH_REMATCH[1]}" 286 | else 287 | printf "${red}获取用户信息异常...${reset}\n" 288 | return 1 289 | fi 290 | user_info=$(grep "/notifications" $TEMP_FILE) 291 | reg_user='balance_area.*>[ ]*([0-9]+)[ ]*[ ]*([0-9]+)[ ]*(.+)$' 292 | if [[ $user_info =~ $reg_user ]]; then 293 | silver=${BASH_REMATCH[1]} 294 | bronze=${BASH_REMATCH[2]} 295 | notifi=${BASH_REMATCH[3]} 296 | if [[ notifi =~ [1-9] ]]; then 297 | notifi="$notifi (https://www.v2ex.com/notifications)" 298 | fi 299 | printf "$pink$USER_NAME$reset $green$silver 银币 $bronze 铜币 $notifi$reset\n" 300 | else 301 | # 只有银币的情况 302 | reg_user='balance_area.*>[ ]*([0-9]+)[ ]*.*/notifications.*>(.+)$' 303 | if [[ $user_info =~ $reg_user ]]; then 304 | silver=${BASH_REMATCH[1]} 305 | notifi=${BASH_REMATCH[2]} 306 | if [[ notifi =~ [1-9] ]]; then 307 | notifi="$notifi (https://www.v2ex.com/notifications)" 308 | fi 309 | printf "$pink$USER_NAME$reset $green$silver 银币 $notifi$reset\n" 310 | else 311 | printf "${red}获取用户信息异常...${reset}\n" 312 | return 1 313 | fi 314 | fi 315 | LOGIN_STATE=1 316 | return 0 317 | } 318 | 319 | _daily() { 320 | if ! test $HTTP; then 321 | printf "${red}签到需要依赖的执行程序httpie不存在,安装参考README.md${reset}\n" 322 | return 1 323 | fi 324 | if [ $LOGIN_STATE -eq 0 ]; then 325 | _login 326 | fi 327 | user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36" 328 | mission_daily="https://www.v2ex.com/mission/daily" 329 | if test "$HTTP"; then 330 | $HTTP --session v2ex $mission_daily "Referer:https://www.v2ex.com" "User-Agent:$user_agent" > $TEMP_FILE 331 | else 332 | curl -s -o $TEMP_FILE -b $COOKIE_FILE -c $COOKIE_FILE -e "https://www.v2ex.com" -A "$user_agent" $mission_daily 333 | fi 334 | redeem=$(grep 'mission/daily/redeem' $TEMP_FILE) 335 | if ! test "$redeem"; then 336 | printf "${green}今天已经签到...${reset}\n" 337 | return 1 338 | fi 339 | reg_redeem="/mission/daily/redeem\?once=(.*)'" 340 | if [[ $redeem =~ $reg_redeem ]]; then 341 | if test "$HTTP"; then 342 | $HTTP --session v2ex -f GET "https://www.v2ex.com/mission/daily/redeem?once=${BASH_REMATCH[1]}" "Referer:$mission_daily" "User-Agent:$user_agent" > $TEMP_FILE 343 | $HTTP --session v2ex $mission_daily "Referer:$mission_daily" "User-Agent:$user_agent" > $TEMP_FILE 344 | else 345 | curl -s -o $TEMP_FILE -b $COOKIE_FILE -c $COOKIE_FILE -e $mission_daily -A "$user_agent" "https://www.v2ex.com/mission/daily/redeem?once=${BASH_REMATCH[1]}" 346 | curl -s -o $TEMP_FILE -b $COOKIE_FILE -c $COOKIE_FILE -e $mission_daily -A "$user_agent" $mission_daily 347 | fi 348 | grep '每日登录奖励已领取' $TEMP_FILE > /dev/null 349 | if [ $? -eq 0 ]; then 350 | cont=$(grep '已连续登录' $TEMP_FILE) 351 | reg_cont="(已连续登录.*天)" 352 | if [[ $cont =~ $reg_cont ]]; then 353 | printf "${green}签到成功,${BASH_REMATCH[1]}${reset}\n" 354 | return 0 355 | fi 356 | fi 357 | fi 358 | printf "${red}签到异常...${reset}\n" 359 | return 1 360 | } 361 | 362 | _test() { 363 | printf "test...\n" 364 | } 365 | 366 | _usage() { 367 | cat << EOF 368 | --------------------------------------------------------------------------------------------------------------- 369 | hot | 热门主题 370 | late | 最新主题 371 | login/relogin | 登录/重新登录 372 | daily | 领取每日签到奖励 373 | cate | 获取指定分类的主题 374 | node | 获取节点的主题 375 | | 查看指定主题序号的所有回复 376 | open | 使用默认浏览器查看指定主题贴 377 | help | 查看帮助 378 | q|quit | 退出 379 | --------------------------------------------------------------------------------------------------------------- 380 | EOF 381 | } 382 | 383 | type jq >/dev/null 2>/dev/null 384 | if [ $? != 0 ]; then 385 | printf "${red}脚本依赖的执行程序jq不存在,安装参考README.md${reset}\n" 386 | exit 1 387 | fi 388 | 389 | type http >/dev/null 2>/dev/null 390 | if [ $? = 0 ]; then 391 | HTTP=http 392 | fi 393 | 394 | while true 395 | do 396 | if [ $# -gt 0 ]; then 397 | data=$@ 398 | else 399 | UPMODE=$(echo $MODE | tr "[:lower:]" "[:upper:]") 400 | if test "$USER_NAME"; then 401 | printf "($pink$USER_NAME$reset) $UPMODE # " 402 | else 403 | printf "$UPMODE # " 404 | fi 405 | read data 406 | if ! test "$data"; then 407 | continue 408 | fi 409 | fi 410 | op=$(echo $data | cut -d " " -f 1) 411 | case "$op" in 412 | q | quit) 413 | break 414 | ;; 415 | late) 416 | _topics topics latest 417 | MODE=$op 418 | ;; 419 | hot) 420 | _topics topics hot 421 | MODE=$op 422 | ;; 423 | cate) 424 | name=$(echo $data | cut -d " " -f 2) 425 | if [ $name != $op ]; then 426 | _categories $name 427 | MODE=$op 428 | else 429 | printf "${red}使用cate 格式${reset}\n" 430 | fi 431 | ;; 432 | login | relogin) 433 | if [ $op = "relogin" ]; then 434 | _login 1 435 | else 436 | _login 437 | fi 438 | if [ $? -eq 0 ]; then 439 | MODE="LOGIN" 440 | fi 441 | ;; 442 | daily) 443 | _daily 444 | ;; 445 | node) 446 | node=$(echo $data | cut -d " " -f 2) 447 | if [ $node != $op ]; then 448 | _topics node $node 449 | MODE=$op 450 | else 451 | printf "${red}使用node 格式${reset}\n" 452 | fi 453 | ;; 454 | open) 455 | idx=$(echo $data | cut -d " " -f 2) 456 | if [ $idx != $op ]; then 457 | _open $idx 458 | MODE=$op 459 | else 460 | printf "${red}使用open 格式${reset}\n" 461 | fi 462 | ;; 463 | help) 464 | _usage 465 | ;; 466 | test) 467 | _test 468 | exit 0 469 | ;; 470 | *) 471 | if [ $op -eq $op ] 2>/dev/null ; then 472 | # 是数字 473 | _sel $op 474 | else 475 | _usage 476 | fi 477 | ;; 478 | esac 479 | if [ $# -gt 0 ]; then 480 | break 481 | fi 482 | done 483 | --------------------------------------------------------------------------------