├── Dockerfile ├── .prettierrc ├── src ├── exit_code.ts ├── util.ts ├── types.ts ├── index.ts └── pinkoi-bot.ts ├── .vscode └── settings.json ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── package.json ├── docs └── cookie_instruction.md ├── LICENSE ├── .gitignore ├── README.md ├── pnpm-lock.yaml └── tsconfig.json /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | COPY dist /app 4 | 5 | ENV TZ=Asia/Taipei 6 | WORKDIR /app 7 | ENTRYPOINT [ "node", "index.js" ] 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /src/exit_code.ts: -------------------------------------------------------------------------------- 1 | export const EXIT_TASK_FAILED = 1 2 | export const EXIT_LOGIN_FAILED = 69 3 | export const EXIT_CODE_INVALID_ARGUMENT = 87 4 | export const EXIT_CODE_UNKNOWN_ERROR = 255 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.schemas": { 3 | "https://json.schemastore.org/github-workflow.json": "file:///home/hyperbola/repo/pinkoi-coins-bot/.github/workflows/build.yml" 4 | }, 5 | "cSpell.words": [ 6 | "pinkoi", 7 | "favlist" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "typescript" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinkoi-coins-bot", 3 | "version": "1.4.0", 4 | "private": true, 5 | "description": "Get pinkoi coins everyday.", 6 | "author": { 7 | "name": "Hyperbola", 8 | "email": "me@hyperbola.me", 9 | "url": "https://github.com/wdzeng" 10 | }, 11 | "homepage": "https://github.com/wdzeng/pinkoi-coins-bot", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/wdzeng/pinkoi-coins-bot.git" 15 | }, 16 | "packageManager": "pnpm@7.29.3", 17 | "scripts": { 18 | "build": "ncc build src/index.ts -o dist -m" 19 | }, 20 | "dependencies": { 21 | "axios": "^1.3.4", 22 | "commander": "^10.0.0", 23 | "isobject": "^4.0.0", 24 | "loglevel": "^1.8.1" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^18.15.3", 28 | "@vercel/ncc": "^0.36.1", 29 | "typescript": "^4.9.5" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/wdzeng/pinkoi-coins-bot/issues" 33 | }, 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /docs/cookie_instruction.md: -------------------------------------------------------------------------------- 1 | # 拿到 Cookie 的方式 2 | 3 | 這篇文章說明如何拿到自己的 Pinkoi cookie。拿到 cookie 之後,就可以把 cookie 餵給機器人做自動化登入。這篇以 Chrome 瀏覽器為範例。 4 | 5 | ## 步驟一、登入 Pinkoi 6 | 7 | 首先到 [Pinkoi 首頁](https://www.pinkoi.com/)並登入你的帳號。 8 | 9 | ## 步驟二、打開開發者工具 10 | 11 | 請按下 `F12` 鍵,此時 chrome 視窗的右邊會跳出管理者工具,如下圖。 12 | 13 | ![image](https://user-images.githubusercontent.com/39057640/173961247-ef9e6ef3-de6e-4a25-99fb-75cc9e8ad61f.png) 14 | 15 | ## 步驟三、切換到網路頁籤 16 | 17 | 如下圖,選擇「網路」和「文件」標籤。 18 | 19 | ![image](https://user-images.githubusercontent.com/39057640/173961616-4c8df04f-8bdb-4dc4-9038-780d7523e641.png) 20 | 21 | ## 步驟四、重新整理網頁 22 | 23 | 按 `F5` 重新整理網頁,此時會出現一個項目,如下圖。 24 | 25 | ![image](https://user-images.githubusercontent.com/39057640/173961727-ee17f3d8-962b-412f-b533-ca8dd96542bc.png) 26 | 27 | ## 步驟五、拿 Cookie 28 | 29 | 承上,給它按下去,可以看見 cookie。複製時確保只有一行,不是以空格開頭或結尾。另外,不要複製到最一開始的 `cookie:`。 30 | 31 | ![image](https://user-images.githubusercontent.com/39057640/173962283-4cc99c35-892a-4833-a18f-5ecc9cc20dcf.png) 32 | 33 | > **Warning** 34 | > Cookie 是機密資訊,不要曝光。 35 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | import log from 'loglevel' // cspell: ignore loglevel 3 | 4 | import { EXIT_CODE_INVALID_ARGUMENT } from './exit_code' 5 | 6 | export function sleep() { 7 | return new Promise((res) => setTimeout(res, 500)) 8 | } 9 | 10 | export function setupLogging(args: OptionValues) { 11 | if (process.env['DEBUG']) { 12 | log.setDefaultLevel('debug') 13 | if (args.quiet) { 14 | log.warn('Option `--quiet` is ignored in debug mode.') 15 | } 16 | } else if (args.quiet) { 17 | log.setDefaultLevel('warn') 18 | } else { 19 | log.setDefaultLevel('info') 20 | } 21 | } 22 | 23 | function requireCheckinOrSolveWeeklyMission(args: OptionValues) { 24 | if (args.checkin === args.solveWeeklyMission) { 25 | log.error( 26 | 'You should run exactly one of --checkin or --solve-weekly-mission.' 27 | ) 28 | process.exit(EXIT_CODE_INVALID_ARGUMENT) 29 | } 30 | } 31 | 32 | export function validateArgs(args: OptionValues) { 33 | requireCheckinOrSolveWeeklyMission(args) 34 | } 35 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface PinkoiErrorResponse { 2 | error: { 3 | code: number 4 | message: string 5 | } 6 | } 7 | 8 | export interface PinkoiValidResponse { 9 | result: T[] 10 | } 11 | 12 | export type PinkoiResponse = 13 | | PinkoiErrorResponse 14 | | PinkoiValidResponse 15 | 16 | export interface User { 17 | email: string 18 | nick: string 19 | } 20 | 21 | export interface WeeklyMission { 22 | mission_key: string 23 | introduction: string 24 | achieved: boolean 25 | redeemed: boolean 26 | } 27 | 28 | export interface Redeem { 29 | // cspell:ignore successed 30 | successed: boolean 31 | } 32 | 33 | export interface SignResult { 34 | reward: number 35 | special: boolean 36 | signed: boolean 37 | } 38 | 39 | export interface Sign { 40 | 0: SignResult 41 | 1: SignResult 42 | 2: SignResult 43 | 3: SignResult 44 | 4: SignResult 45 | 5: SignResult 46 | 6: SignResult 47 | } 48 | 49 | export interface InMissionPeriod { 50 | in_mission_period: boolean 51 | } 52 | 53 | export interface FavList { 54 | favlist_id: string 55 | name: string 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 hyperbola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: ~ 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - src/** 10 | 11 | jobs: 12 | build: 13 | name: Build and push image onto dockerhub and ghcr 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - name: Install pnpm and dependencies 19 | uses: pnpm/action-setup@v2.2.2 20 | with: 21 | run_install: true 22 | - name: Build project 23 | run: pnpm build 24 | - uses: wdzeng/image@v2 25 | with: 26 | dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | dockerhub-password: ${{ secrets.DOCKERHUB_PASSWORD }} 28 | repo-description: Get pinkoi coins everyday 29 | repo-license: MIT 30 | tag: 31 | name: Add tags to repository 32 | if: ${{ github.event_name == 'push' }} 33 | needs: 34 | - build 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | - id: version 39 | uses: wdzeng/version@v1 40 | with: 41 | prefix: v 42 | - name: Add tags 43 | run: | 44 | git tag -f ${{ steps.version.outputs.version }} main && \ 45 | git push -f origin ${{ steps.version.outputs.version }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pinkoi 簽到機器人 2 | 3 | [![release](https://badgen.net/github/release/wdzeng/pinkoi-coins-bot/stable?color=red)](https://github.com/wdzeng/pinkoi-coins-bot/releases/latest) 4 | [![github](https://badgen.net/badge/icon/github/black?icon=github&label=)](https://github.com/wdzeng/pinkoi-coins-bot) 5 | [![docker](https://badgen.net/badge/icon/docker?icon=docker&label=)](https://hub.docker.com/repository/docker/hyperbola/pinkoi-coins-bot) 6 | 7 | 💰💰 每日簽到 [Pinkoi](https://www.pinkoi.com) 的機器人 💰💰 8 | 9 | ## 執行方式 10 | 11 | 請先安裝 [docker](https://docker.com) 或 [podman](https://podman.io/)。映像可於 Docker Hub `hyperbola/pinkoi-coins-bot` 或 GitHub Container Registry (ghcr) [`ghcr.io/wdzeng/pinkoi-coins-bot`](https://ghcr.io/wdzeng/pinkoi-coins-bot) 取得。最新的 tag 為 `1`,tag 清單可見於 Docker Hub 或 ghcr。 12 | 13 | Pinkoi 登入的方式眾多。由於沒有辦法模擬 Google 帳號以及其他第三方平台登入的情況,使用者需要自備 cookie 給機器人登入。請將 cookie 字串存在檔案中餵給機器人。Pinkoi 的 cookie 通常會長得像下面這樣,注意一定只有一行。 14 | 15 | ```txt 16 | slocale=1; lang=zh_TW; b=20220603xxxxxxxx; __zlcmid=xxxxxxxxxxxxx; sessionid=xxxxxxxxxxxxxxxxxxxxxxxxxxx; sv=1.0; stv=1.0; ad=0; geo=TW; ci=HSQ; tz="Asia/Taipei"; c=TWD; country_code=TW; st=b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; campaign=mission_game 17 | ``` 18 | 19 | 關於 cookie 的取得方式,請參考這份[文件](docs/cookie_instruction.md)或這個[影片](https://www.youtube.com/watch?v=E-j-vlDuYtA)。 20 | 21 | > **Warning** 22 | > Cookie 是機密資訊,請妥善保存。 23 | 24 | ### 使用說明 25 | 26 | 使用 `--help` 參數。 27 | 28 | ```sh 29 | docker run -it hyperbola/pinkoi-coins-bot:1 --help 30 | ``` 31 | 32 | ### 每日簽到 33 | 34 | 使用 `--checkin` 參數。 35 | 36 | ```sh 37 | docker run [-it] \ 38 | -v /path/to/cookie:/cookie \ 39 | hyperbola/pinkoi-coins-bot:1 --cookie /cookie --checkin 40 | ``` 41 | 42 | ### 解週末任務 43 | 44 | 使用 `--solve-weekly-mission` 參數。 45 | 46 | ```sh 47 | docker run [-it] \ 48 | -v /path/to/cookie:/cookie \ 49 | hyperbola/pinkoi-coins-bot:1 --cookie /cookie --solve-weekly-mission 50 | ``` 51 | 52 | ## 參數 53 | 54 | 執行機器人時,必須從 `--checkin` 或 `--solve-weekly-mission` 中選擇恰一個執行。Cookie 是必填。 55 | 56 | - `-c`, `--cookie`: cookie 檔案位置 57 | - `-s`, `--checkin`: 每日簽到 58 | - `-m`, `--solve-weekly-mission`: 解週末任務 59 | - `-q`, `--quiet`: 不要印出訊息;警告和錯誤訊息仍會印出 60 | - `-V`, `--version`: 印出版本 61 | - `-h`, `--help`: 印出使用方式 62 | 63 | ## Exit Code 64 | 65 | | Exit Code | 解釋 | 66 | | --- | ---- | 67 | | 0 | 簽到或任務成功。 | 68 | | 1 | 簽到或任務失敗。 | 69 | | 69 | 登入失敗。這表示 cookie 有問題。 | 70 | | 87 | 參數錯誤。 | 71 | | 255 | 不明錯誤。 | 72 | 73 | ## 姊妹機器人 74 | 75 | - [蝦皮簽到機器人](https://github.com/wdzeng/shopee-coins-bot/) 76 | - [批踢踢登入機器人](https://github.com/wdzeng/ptt-login-bot/) 77 | - [Telegram ID 覬覦者](https://github.com/wdzeng/telegram-id-pretender/) 78 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | 4 | import { program } from 'commander' 5 | import log from 'loglevel' // cspell: ignore loglevel 6 | 7 | import { 8 | EXIT_CODE_INVALID_ARGUMENT, 9 | EXIT_CODE_UNKNOWN_ERROR, 10 | EXIT_LOGIN_FAILED, 11 | EXIT_TASK_FAILED 12 | } from './exit_code' 13 | import Bot from './pinkoi-bot' 14 | import { setupLogging, validateArgs } from './util' 15 | 16 | const version = '1.4.0' 17 | const majorVersion = version.split('.')[0] 18 | 19 | program 20 | .name( 21 | 'docker run -v /path/to/cookie:/cookie -it hyperbola/pinkoi-coins-bot:' + 22 | majorVersion 23 | ) 24 | .usage('--cookie /cookie [--checkin | --solve-weekly-usage]') 25 | .description('A bot to checkin to get Pinkoi coins.') 26 | .requiredOption('-c, --cookie ', 'path to cookie') 27 | .option('-s, --checkin', 'checkin Pinkoi') 28 | .option('-m, --solve-weekly-mission', 'solve Pinkoi weekly mission') 29 | .option('-q, --quiet', 'do not print messages') 30 | .helpOption('-h, --help', 'show this message') 31 | .version(version, '-V, --version') 32 | .exitOverride((e) => 33 | process.exit(e.exitCode === 1 ? EXIT_CODE_INVALID_ARGUMENT : e.exitCode) 34 | ) 35 | 36 | const args = program.parse(process.argv).opts() 37 | validateArgs(args) 38 | 39 | setupLogging(args) 40 | 41 | async function main() { 42 | log.info('Start pinkoi coins bot v' + version + '.') 43 | 44 | // Load cookie. 45 | log.debug('Load cookie from: ' + args.cookie) 46 | let cookie: string 47 | try { 48 | cookie = fs.readFileSync(path.resolve(args.cookie), 'utf-8').trim() 49 | } catch (e: unknown) { 50 | log.error('Failed to read cookie: ' + args.cookie) 51 | if (e instanceof Error) { 52 | log.error(e.message) 53 | } 54 | process.exit(EXIT_CODE_INVALID_ARGUMENT) 55 | } 56 | log.info('Cookie loaded.') 57 | log.debug('Use cookie: ' + cookie.slice(0, 128) + ' [truncated]') 58 | 59 | // Create a bot. 60 | const bot = new Bot(cookie) 61 | 62 | // Check user login. 63 | log.debug('Check user login.') 64 | const user = await bot.getUser() 65 | if (user === undefined) { 66 | log.error('Login failed. Please check your cookie.') 67 | process.exit(EXIT_LOGIN_FAILED) 68 | } 69 | log.info(`Login as ${user.nick} <${user.email}>.`) 70 | 71 | // Run bot. 72 | try { 73 | await (args.checkin ? bot.checkin() : bot.solveWeeklyMission()) 74 | } catch (e: unknown) { 75 | if (e instanceof Error) { 76 | log.error(e.message) 77 | log.debug(e.stack) 78 | process.exit(EXIT_TASK_FAILED) 79 | } 80 | 81 | // Unknown error 82 | log.error('Task failed: unknown error') 83 | log.debug(e) 84 | process.exit(EXIT_CODE_UNKNOWN_ERROR) 85 | } 86 | 87 | log.info('Bye Bye!') 88 | } 89 | 90 | main() 91 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | workflow_dispatch: ~ 16 | push: 17 | branches: [ "main" ] 18 | paths: [ "src/**" ] 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: [ "main" ] 22 | schedule: 23 | - cron: '20 7 * * 3' 24 | 25 | jobs: 26 | analyze: 27 | name: Analyze 28 | runs-on: ubuntu-latest 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'javascript' ] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@types/node': ^18.15.3 5 | '@vercel/ncc': ^0.36.1 6 | axios: ^1.3.4 7 | commander: ^10.0.0 8 | isobject: ^4.0.0 9 | loglevel: ^1.8.1 10 | typescript: ^4.9.5 11 | 12 | dependencies: 13 | axios: 1.3.4 14 | commander: 10.0.0 15 | isobject: 4.0.0 16 | loglevel: 1.8.1 17 | 18 | devDependencies: 19 | '@types/node': 18.15.3 20 | '@vercel/ncc': 0.36.1 21 | typescript: 4.9.5 22 | 23 | packages: 24 | 25 | /@types/node/18.15.3: 26 | resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==} 27 | dev: true 28 | 29 | /@vercel/ncc/0.36.1: 30 | resolution: {integrity: sha512-S4cL7Taa9yb5qbv+6wLgiKVZ03Qfkc4jGRuiUQMQ8HGBD5pcNRnHeYM33zBvJE4/zJGjJJ8GScB+WmTsn9mORw==} 31 | hasBin: true 32 | dev: true 33 | 34 | /asynckit/0.4.0: 35 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 36 | dev: false 37 | 38 | /axios/1.3.4: 39 | resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==} 40 | dependencies: 41 | follow-redirects: 1.15.2 42 | form-data: 4.0.0 43 | proxy-from-env: 1.1.0 44 | transitivePeerDependencies: 45 | - debug 46 | dev: false 47 | 48 | /combined-stream/1.0.8: 49 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 50 | engines: {node: '>= 0.8'} 51 | dependencies: 52 | delayed-stream: 1.0.0 53 | dev: false 54 | 55 | /commander/10.0.0: 56 | resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} 57 | engines: {node: '>=14'} 58 | dev: false 59 | 60 | /delayed-stream/1.0.0: 61 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 62 | engines: {node: '>=0.4.0'} 63 | dev: false 64 | 65 | /follow-redirects/1.15.2: 66 | resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} 67 | engines: {node: '>=4.0'} 68 | peerDependencies: 69 | debug: '*' 70 | peerDependenciesMeta: 71 | debug: 72 | optional: true 73 | dev: false 74 | 75 | /form-data/4.0.0: 76 | resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} 77 | engines: {node: '>= 6'} 78 | dependencies: 79 | asynckit: 0.4.0 80 | combined-stream: 1.0.8 81 | mime-types: 2.1.35 82 | dev: false 83 | 84 | /isobject/4.0.0: 85 | resolution: {integrity: sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==} 86 | engines: {node: '>=0.10.0'} 87 | dev: false 88 | 89 | /loglevel/1.8.1: 90 | resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} 91 | engines: {node: '>= 0.6.0'} 92 | dev: false 93 | 94 | /mime-db/1.52.0: 95 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 96 | engines: {node: '>= 0.6'} 97 | dev: false 98 | 99 | /mime-types/2.1.35: 100 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 101 | engines: {node: '>= 0.6'} 102 | dependencies: 103 | mime-db: 1.52.0 104 | dev: false 105 | 106 | /proxy-from-env/1.1.0: 107 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 108 | dev: false 109 | 110 | /typescript/4.9.5: 111 | resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} 112 | engines: {node: '>=4.2.0'} 113 | hasBin: true 114 | dev: true 115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist", /* Redirect output structure to the directory. */ 15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | /* Additional Checks */ 33 | "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | /* Module Resolution Options */ 38 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 39 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 40 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 41 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 42 | // "typeRoots": [], /* List of folders to include type definitions from. */ 43 | // "types": [], /* Type declaration files to be included in compilation. */ 44 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 45 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 46 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 47 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 48 | /* Source Map Options */ 49 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 50 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 51 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 52 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 53 | /* Experimental Options */ 54 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 55 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 56 | }, 57 | "exclude": [ 58 | "node_modules" 59 | ] 60 | } -------------------------------------------------------------------------------- /src/pinkoi-bot.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosResponse } from 'axios' 2 | import isobject from 'isobject' 3 | import log from 'loglevel' // c-spell: ignore loglevel 4 | 5 | import { 6 | FavList, 7 | InMissionPeriod, 8 | PinkoiResponse, 9 | PinkoiValidResponse, 10 | Redeem, 11 | Sign, 12 | SignResult, 13 | User, 14 | WeeklyMission 15 | } from './types' 16 | import { sleep } from './util' 17 | 18 | const missionKeyNames = [ 19 | 'view_topic', 20 | 'add_fav_shop', 21 | 'add_fav_item', 22 | 'weekly_bonus' 23 | ] 24 | const referer = 'https://www.pinkoi.com/event/mission_game' 25 | 26 | function outdate(): never { 27 | log.error( 28 | 'Unexpected mission content. Maybe this bot is outdated. Try passing environment variable DEBUG=1 to see what occurred.' 29 | ) 30 | throw new Error('unexpected mission content') 31 | } 32 | 33 | function validateWeeklyMissionContent( 34 | _mission: PinkoiValidResponse 35 | ): asserts _mission is PinkoiValidResponse { 36 | log.debug('Check if mission content is as expected.') 37 | 38 | if (!isobject(_mission)) { 39 | log.debug(_mission) 40 | outdate() 41 | } 42 | 43 | const mission: any = _mission 44 | const missionCount: unknown = mission?.result?.length 45 | if (missionCount !== missionKeyNames.length) { 46 | log.debug( 47 | `Expected ${missionKeyNames.length} missions but got ${missionCount}.` 48 | ) 49 | log.debug(mission) 50 | outdate() 51 | } 52 | 53 | function validateMissionKey(index: number, expectedKey: string) { 54 | const keyName: unknown = mission.result[index]['mission_key'] 55 | if (keyName !== expectedKey) { 56 | log.debug( 57 | 'Unexpected mission key: ' + keyName + '; should be ' + expectedKey 58 | ) 59 | outdate() 60 | } 61 | } 62 | 63 | missionKeyNames.map((keyName, index) => validateMissionKey(index, keyName)) 64 | 65 | log.debug('Get expected mission content.') 66 | } 67 | 68 | function getWeeklyMissionStatus(missionList: WeeklyMission[]): (0 | 1 | 2)[] { 69 | // 0: not solved 70 | // 1: solved; not redeemed 71 | // 2: redeemed 72 | return missionList.map((x) => (x.redeemed ? 2 : x.achieved ? 1 : 0)) 73 | } 74 | 75 | function validatePinkoiResponse( 76 | response: AxiosResponse> 77 | ): asserts response is AxiosResponse> { 78 | if ('error' in response.data) { 79 | log.debug(JSON.stringify(response.data)) 80 | throw new Error('pinkoi: ' + response.data.error.message) 81 | } 82 | // No error 83 | } 84 | 85 | function handleMissionError(missionKey: string, e: unknown): never { 86 | if (e instanceof Error) { 87 | throw new Error(`${missionKey}: ` + e.message) 88 | } 89 | 90 | // Unexpected error 91 | log.debug(e) 92 | throw new Error(`${missionKey}: unknown error`) 93 | } 94 | 95 | export default class PinkoiBot { 96 | constructor(private readonly cookie: string) {} 97 | 98 | private async solveViewTopic(mission: WeeklyMission): Promise { 99 | // 點擊瀏覽當季的活動頁 👉 週末放假靈感|手作地毯・流動畫 100 | 101 | const missionKey = mission.mission_key 102 | log.debug('Solving mission: %s', missionKey) 103 | 104 | const headers = { cookie: this.cookie, referer } 105 | const urlRegex = 106 | /https:\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w\p{Script=Han}.,@?^=%&:\/~+#-]*[\w\p{Script=Han}@?^=%&\/~+#-])/gu 107 | try { 108 | const urls: string[] | null = mission.introduction.match(urlRegex) 109 | log.debug('%s: got URLs: %s', missionKey, JSON.stringify(urls)) 110 | 111 | if (urls?.length !== 1) { 112 | log.error('Found more than one URLs: %s', JSON.stringify(urls)) 113 | throw new Error(`Expected 1 URL; found ${urls?.length}.`) 114 | } 115 | 116 | log.debug('%s: clicking URL: %s', missionKey, urls[0]) 117 | await axios.get(urls[0], { headers }) 118 | log.debug('%s: URL clicked: %s', missionKey, urls[0]) 119 | await sleep() 120 | 121 | log.info('Mission completed: %s', missionKey) 122 | } catch (e: unknown) { 123 | handleMissionError(missionKey, e) 124 | } 125 | } 126 | 127 | private async addFavShop(shopId: string): Promise { 128 | const url = 'https://www.pinkoi.com/apiv2/shop/fav' 129 | const body = { sid: shopId } 130 | const headers = { cookie: this.cookie, referer } 131 | log.debug('Adding fav shop: %s', shopId) 132 | await axios.post(url, body, { headers }) 133 | log.debug('Added fav shop: %s', shopId) 134 | } 135 | 136 | private async removeFavShop(shopId: string): Promise { 137 | const url = 'https://www.pinkoi.com/apiv2/shop/unfav' 138 | const body = { sid: shopId } 139 | const headers = { cookie: this.cookie, referer } 140 | log.debug('Removing fav shop: %s', shopId) 141 | await axios.post(url, body, { headers }) 142 | log.debug('Removed fav shop: %s', shopId) 143 | } 144 | 145 | private async solveAddFavShop(mission: WeeklyMission): Promise { 146 | // 關注 1 間設計館 👉 馬上看 你的專屬推薦 147 | // 點擊查看設計館頁,並完成 1 次關注。
\n👉 任務頁面下方有「為你推薦的品牌及商品」,快去看看吧 148 | 149 | const missionKey = mission.mission_key 150 | log.debug('Solving mission: %s', missionKey) 151 | 152 | const shopId = 'ekax' 153 | try { 154 | await this.addFavShop(shopId) 155 | await sleep() 156 | await this.removeFavShop(shopId) 157 | await sleep() 158 | } catch (e: unknown) { 159 | handleMissionError(missionKey, e) 160 | } 161 | 162 | log.info('Mission solved: %s', missionKey) 163 | } 164 | 165 | private async createFavList(favListName: string): Promise { 166 | log.debug('Creating new fav list: %s', favListName) 167 | 168 | const url = 'https://www.pinkoi.com/apiv3/favlist/add' 169 | const body = { name: favListName, is_public: 0 } 170 | const headers = { cookie: this.cookie, referer } 171 | 172 | // Note that the response is not wrapped (not PinkoiResponse) 173 | const res = await axios.post(url, body, { headers }) 174 | const favListId = res.data.favlist_id 175 | 176 | log.debug('Fav list created: %s, ID: %d', favListName, favListId) 177 | return favListId 178 | } 179 | 180 | private async removeFavList(favListId: string): Promise { 181 | log.debug('Removing fav list: %s', favListId) 182 | 183 | const url = 'https://www.pinkoi.com/apiv3/favlist/delete' 184 | // cspell: ignore unfav 185 | const body = { favlist_id: favListId, unfav_all: true } 186 | const headers = { cookie: this.cookie, referer } 187 | const res2 = await axios.post(url, body, { headers }) 188 | validatePinkoiResponse(res2) 189 | 190 | log.debug('Fav list removed: %s', favListId) 191 | } 192 | 193 | private async addFavItem(itemId: string, favListId: string): Promise { 194 | log.debug('Adding fav item: %s', itemId) 195 | 196 | const url = 'https://www.pinkoi.com/apiv3/item/fav' 197 | const body = { favlist_id: favListId, tid: itemId } 198 | const headers = { cookie: this.cookie, referer } 199 | await axios.post(url, body, { headers }) 200 | 201 | log.debug('Fav item added: %s', itemId) 202 | } 203 | 204 | private async solveAddFavItem(mission: WeeklyMission): Promise { 205 | // 點擊查看商品,並完成 3 次收藏 👉 馬上看 你的專屬推薦
任務進度:0 / 3 206 | // 點擊查看商品,並完成 3 次收藏。
\n👉 任務頁面下方有「為你推薦的品牌及商品」,快去看看吧!
任務進度:已達成 0 / 3 207 | 208 | const missionKey = mission.mission_key 209 | const favListName = 'pinkoi-coins-bot' 210 | const itemIds = ['6k5tF2uK', 'zDzEKiTR', 'YRcUicek'] // cspell: disable-line 211 | 212 | log.debug('Solving mission: %s', missionKey) 213 | try { 214 | const favListId = await this.createFavList(favListName) 215 | await sleep() 216 | 217 | for (const itemId of itemIds) { 218 | await this.addFavItem(itemId, favListId) 219 | await sleep() 220 | } 221 | 222 | await this.removeFavList(favListId) 223 | await sleep() 224 | 225 | log.info('Mission completed: %s;', missionKey) 226 | } catch (e: unknown) { 227 | handleMissionError(missionKey, e) 228 | } 229 | } 230 | 231 | async requireInWeeklyMissionPeriod(): Promise { 232 | // Check if it is mission period now. 233 | 234 | log.debug('Checking if is in mission period now.') 235 | 236 | const url = 'https://www.pinkoi.com/apiv2/mission_game/in_mission_period' 237 | const response = await axios.get>(url) 238 | validatePinkoiResponse(response) 239 | 240 | if (response.data.result[0].in_mission_period !== true) { 241 | throw new Error('Not in weekly mission period now.') 242 | } 243 | 244 | log.debug('In mission period.') 245 | } 246 | 247 | private async getWeeklyMissionList(): Promise { 248 | // Get mission list. 249 | const url = 'https://www.pinkoi.com/apiv2/mission_game/mission_list' 250 | const response = await axios.get(url, { 251 | headers: { cookie: this.cookie, referer } 252 | }) 253 | validatePinkoiResponse(response) 254 | 255 | // Validate mission list content. 256 | validateWeeklyMissionContent(response.data) 257 | return response.data.result 258 | } 259 | 260 | private async redeemWeeklyMission(missionKey: string): Promise { 261 | log.debug('Redeem mission: %s', missionKey) 262 | 263 | try { 264 | const url = 'https://www.pinkoi.com/apiv2/mission_game/redeem' 265 | const body = { mission_key: missionKey } 266 | const response = await axios.post>(url, body, { 267 | headers: { cookie: this.cookie, referer } 268 | }) 269 | validatePinkoiResponse(response) 270 | 271 | const result = response.data 272 | log.debug(JSON.stringify(result)) 273 | 274 | if (result.result[0].successed !== true) { 275 | // c-spell: ignore successed 276 | if (process.env['STRICT']) { 277 | throw new Error('Mission not completed.') 278 | } else { 279 | log.warn( 280 | 'Mission %s not redeemed. \ 281 | This may be concurrency issue on Pinkoi server. Keep going.', 282 | missionKey 283 | ) 284 | } 285 | } else { 286 | log.info('Mission redeemed: %s', missionKey) 287 | } 288 | } catch (e) { 289 | if (e instanceof Error) { 290 | log.error('Mission not redeemed: %s: %s', missionKey, e.message) 291 | if (e instanceof AxiosError) { 292 | log.debug(JSON.stringify(e.response?.data)) 293 | } 294 | } else { 295 | log.error('Mission not redeemed: %s: unknown error', missionKey) 296 | } 297 | throw e 298 | } 299 | } 300 | 301 | async solveWeeklyMission(): Promise { 302 | try { 303 | // Require in mission period. 304 | await this.requireInWeeklyMissionPeriod() 305 | 306 | // Get mission list. 307 | log.debug('Fetching mission list.') 308 | let missionList = await this.getWeeklyMissionList() 309 | let missionStatus = getWeeklyMissionStatus(missionList) 310 | log.debug('Mission list fetched: %s', JSON.stringify(missionList)) 311 | 312 | // Solve missions if not solved. 313 | function alreadySolved(keyName: string) { 314 | log.info('Mission %s already solved.', keyName) 315 | return Promise.resolve() 316 | } 317 | 318 | await (missionStatus[0] === 0 319 | ? this.solveViewTopic(missionList[0]) 320 | : alreadySolved(missionKeyNames[0])) 321 | await (missionStatus[1] === 0 322 | ? this.solveAddFavShop(missionList[1]) 323 | : alreadySolved(missionKeyNames[1])) 324 | await (missionStatus[2] === 0 325 | ? this.solveAddFavItem(missionList[3]) 326 | : alreadySolved(missionKeyNames[3])) 327 | 328 | // Check if all five missions should have been solved. 329 | // Note: there are bugs on pinkoi server. The mission may be showed 330 | // not solved but can be redeemed. 331 | log.debug('Updating mission status.') 332 | missionList = await this.getWeeklyMissionList() 333 | missionStatus = getWeeklyMissionStatus(missionList) 334 | log.debug('Mission status updated: %d', missionStatus) 335 | 336 | const unsolvedMissions = [] 337 | for (let i of [0, 1, 3]) { 338 | if (missionStatus[i] === 0) unsolvedMissions.push(i) 339 | } 340 | if (unsolvedMissions.length > 0) { 341 | if (process.env['STRICT']) { 342 | throw new Error('Not all missions are solved: ' + unsolvedMissions) 343 | } else { 344 | log.warn('Not all missions are solved: %s', unsolvedMissions) 345 | log.warn( 346 | 'This may be concurrency issue on Pinkoi server. Keep going on.' 347 | ) 348 | } 349 | } else { 350 | log.info('All missions solved.') 351 | } 352 | 353 | // Click redeem buttons for six missions. 354 | for (let i of [0, 1, 3, 2]) { 355 | if (missionStatus[i] === 2) { 356 | log.info('Mission already redeemed: %s', missionKeyNames[i]) 357 | } else { 358 | await this.redeemWeeklyMission(missionKeyNames[i]) 359 | await sleep() 360 | } 361 | } 362 | log.info('All missions redeemed.') 363 | 364 | log.info('Weekly missions all done.') 365 | } catch (e: unknown) { 366 | if (e instanceof AxiosError) { 367 | log.error('Status code: %s', e.response?.status) 368 | log.error(JSON.stringify(e.response?.data)) 369 | log.debug(e) 370 | } 371 | throw e 372 | } 373 | } 374 | 375 | async getCheckinStatus(): Promise { 376 | const url = 'https://www.pinkoi.com/apiv2/mission_game/daily_signin' 377 | const response = await axios.post>(url, undefined, { 378 | headers: { cookie: this.cookie, referer } 379 | }) 380 | validatePinkoiResponse(response) 381 | 382 | const values: SignResult[] = Object.values(response.data.result[0]) 383 | return values.map((e) => e.signed) 384 | } 385 | 386 | async checkin(): Promise { 387 | // Get current day 388 | type Day = 0 | 1 | 2 | 3 | 4 | 5 | 6 389 | let day: Day = new Date().getDay() as Day 390 | // Map Sunday to 6 and Monday - Saturday to 0 - 5 391 | day = (day === 0 ? 6 : day - 1) as Day 392 | log.debug('Today: ' + day) 393 | 394 | const status = await this.getCheckinStatus() 395 | if (!status[day]) { 396 | // Should not happened 397 | throw new Error('Check-in failed: unknown error') 398 | } 399 | } 400 | 401 | async getUser(): Promise<{ email: string; nick: string } | undefined> { 402 | const url = 'https://www.pinkoi.com/apiv2/user/meta' 403 | let response: AxiosResponse> 404 | 405 | try { 406 | response = await axios.get>(url, { 407 | headers: { cookie: this.cookie } 408 | }) 409 | } catch (e: unknown) { 410 | if (e instanceof AxiosError) { 411 | log.debug('AxiosError: ' + e.code) 412 | if (e.code === AxiosError.ERR_FR_TOO_MANY_REDIRECTS) { 413 | // Expired cookies 414 | log.warn('Cookies may have been expired.') 415 | return undefined 416 | } 417 | } 418 | // Unknown error 419 | throw e 420 | } 421 | 422 | if ('error' in response.data && response.data.error.code === 403) { 423 | return undefined // not logged in 424 | } 425 | validatePinkoiResponse(response) 426 | 427 | return response.data.result[0] 428 | } 429 | } 430 | --------------------------------------------------------------------------------