瀏覽代碼

switch to playwright

Mike L 4 年之前
父節點
當前提交
fa72b77253
共有 8 個文件被更改,包括 92 次插入54 次删除
  1. 5 4
      config.example.json
  2. 6 5
      dist/main.js
  3. 2 1
      dist/twitter.js
  4. 31 13
      dist/webshot.js
  5. 1 0
      package.json
  6. 6 5
      src/main.ts
  7. 4 0
      src/twitter.ts
  8. 37 26
      src/webshot.ts

+ 5 - 4
config.example.json

@@ -1,13 +1,14 @@
 {
-  "mirai_access_token": "",
-  "mirai_http_host": "127.0.0.1",
-  "mirai_http_port": 8080,
-  "mirai_bot_qq": 10000,
+  "cq_access_token": "",
+  "cq_http_host": "127.0.0.1",
+  "cq_http_port": 5700,
+  "cq_bot_qq": 10000,
   "twitter_consumer_key": "",
   "twitter_consumer_secret": "",
   "twitter_access_token_key": "",
   "twitter_access_token_secret": "",
   "mode": 0,
+  "playwright_ws_spec_endpoint": "http://127.0.0.1:8080/playwright-ws.json",
   "resume_on_start": false,
   "work_interval": 60,
   "webshot_delay": 10000,

+ 6 - 5
dist/main.js

@@ -50,7 +50,7 @@ const requiredFields = [
     'twitter_consumer_key', 'twitter_consumer_secret', 'twitter_access_token_key', 'twitter_access_token_secret',
 ];
 const warningFields = [
-    'mirai_http_host', 'mirai_http_port', 'mirai_access_token',
+    'cq_http_host', 'cq_http_port', 'cq_access_token',
 ];
 const optionalFields = [
     'lockfile', 'work_interval', 'webshot_delay', 'loglevel', 'mode', 'resume_on_start',
@@ -107,10 +107,10 @@ if (!config.resume_on_start) {
     });
 }
 const qq = new koishi_1.default({
-    access_token: config.mirai_access_token,
-    host: config.mirai_http_host,
-    port: config.mirai_http_port,
-    bot_id: config.mirai_bot_qq,
+    access_token: config.cq_access_token,
+    host: config.cq_http_host,
+    port: config.cq_http_port,
+    bot_id: config.cq_bot_qq,
     list: (c, a, cb) => command_1.list(c, a, cb, lock),
     sub: (c, a, cb) => command_1.sub(c, a, cb, lock, config.lockfile),
     unsub: (c, a, cb) => command_1.unsub(c, a, cb, lock, config.lockfile),
@@ -126,6 +126,7 @@ const worker = new twitter_1.default({
     bot: qq,
     webshotDelay: config.webshot_delay,
     mode: config.mode,
+    wsUrl: config.playwright_ws_spec_endpoint,
 });
 worker.launch();
 qq.connect();

+ 2 - 1
dist/twitter.js

@@ -75,7 +75,7 @@ const retryOnError = (doWork, onRetry) => new Promise(resolve => {
 class default_1 {
     constructor(opt) {
         this.launch = () => {
-            this.webshot = new webshot_1.default(this.mode, () => setTimeout(this.work, this.workInterval * 1000));
+            this.webshot = new webshot_1.default(this.wsUrl, this.mode, () => setTimeout(this.work, this.workInterval * 1000));
         };
         this.queryUser = (username) => this.client.get('users/show', { screen_name: username })
             .then((user) => user.screen_name);
@@ -274,6 +274,7 @@ class default_1 {
         this.bot = opt.bot;
         this.webshotDelay = opt.webshotDelay;
         this.mode = opt.mode;
+        this.wsUrl = opt.wsUrl;
         ScreenNameNormalizer._queryUser = this.queryUser;
         exports.sendTweet = (id, receiver) => {
             this.getTweet(id, this.sendTweets(`tweet ${id}`, receiver))

+ 31 - 13
dist/webshot.js

@@ -5,7 +5,7 @@ const axios_1 = require("axios");
 const CallableInstance = require("callable-instance");
 const html_entities_1 = require("html-entities");
 const pngjs_1 = require("pngjs");
-const puppeteer = require("puppeteer");
+const puppeteer = require("playwright");
 const sharp = require("sharp");
 const loggers_1 = require("./loggers");
 const koishi_1 = require("./koishi");
@@ -25,9 +25,15 @@ const typeInZH = {
 };
 const logger = loggers_1.getLogger('webshot');
 class Webshot extends CallableInstance {
-    constructor(mode, onready) {
+    constructor(wsUrl, mode, onready) {
         super('webshot');
-        this.connect = (onready) => puppeteer.connect({ browserURL: 'http://127.0.0.1:9222' })
+        this.connect = (onready) => axios_1.default.get(this.wsUrl)
+            .then(res => {
+            logger.info(`received websocket endpoint: ${JSON.stringify(res.data)}`);
+            const browserType = Object.keys(res.data)[0];
+            return puppeteer[browserType]
+                .connect({ wsEndpoint: res.data[browserType] });
+        })
             .then(browser => this.browser = browser)
             .then(() => {
             logger.info('launched puppeteer browser');
@@ -53,22 +59,27 @@ class Webshot extends CallableInstance {
                 const width = 720;
                 const zoomFactor = 2;
                 logger.info(`shooting ${width}*${height} webshot for ${url}`);
-                this.browser.newPage()
+                this.browser.newPage({
+                    bypassCSP: true,
+                    deviceScaleFactor: zoomFactor,
+                    locale: 'ja-JP',
+                    timezoneId: 'Asia/Tokyo',
+                    userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
+                })
                     .then(page => {
                     const startTime = new Date().getTime();
                     const getTimerTime = () => new Date().getTime() - startTime;
                     const getTimeout = () => Math.max(500, webshotDelay - getTimerTime());
-                    page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
-                        .then(() => page.setViewport({
+                    page.setViewportSize({
                         width: width / zoomFactor,
                         height: height / zoomFactor,
-                        isMobile: true,
-                        deviceScaleFactor: zoomFactor,
-                    }))
-                        .then(() => page.setBypassCSP(true))
+                    })
                         .then(() => page.goto(url, { waitUntil: 'load', timeout: getTimeout() }))
                         .then(() => page.addStyleTag({
                         content: 'header{display:none!important}path[d=\'M20.207 7.043a1 1 0 0 0-1.414 0L12 13.836 5.207 7.043a1 1 0 0 0-1.414 1.414l7.5 7.5a.996.996 0 0 0 1.414 0l7.5-7.5a1 1 0 0 0 0-1.414z\'],div[role=\'button\']{display: none;}',
+                    }))
+                        .then(() => page.addStyleTag({
+                        content: '*{font-family:-apple-system,".Helvetica Neue DeskInterface",Hiragino Sans,Hiragino Sans GB,sans-serif!important}',
                     }))
                         .then(() => page.evaluate(() => {
                         const poll = setInterval(() => {
@@ -128,7 +139,7 @@ class Webshot extends CallableInstance {
                             const idx = (x, y) => (this.width * y + x) << 2;
                             let boundary = null;
                             let x = zoomFactor * 2;
-                            for (let y = 0; y < this.height; y++) {
+                            for (let y = 0; y < this.height; y += zoomFactor) {
                                 if (this.data[idx(x, y)] !== 255 &&
                                     this.data[idx(x, y)] === this.data[idx(x + zoomFactor * 10, y)]) {
                                     if (this.data[idx(x, y + 18 * zoomFactor)] !== 255) {
@@ -148,7 +159,7 @@ class Webshot extends CallableInstance {
                                 x = Math.floor(16 * zoomFactor);
                                 let flag = false;
                                 let cnt = 0;
-                                for (let y = this.height - 1; y >= 0; y--) {
+                                for (let y = this.height - 1 - zoomFactor; y >= 0; y -= zoomFactor) {
                                     if ((this.data[idx(x, y)] === 255) === flag) {
                                         cnt++;
                                         flag = !flag;
@@ -160,8 +171,14 @@ class Webshot extends CallableInstance {
                                     }
                                     if (cnt === 4) {
                                         const b = y + 1;
-                                        if (this.height - boundary - (boundary - b) <= 1) {
+                                        if (Math.abs(this.height - boundary - (boundary - b)) <= 3 * zoomFactor) {
                                             boundary = b;
+                                        }
+                                    }
+                                    if (cnt === 6) {
+                                        const c = y + 1;
+                                        if (Math.abs(this.height - boundary - 2 * (boundary - c)) <= 3 * zoomFactor) {
+                                            boundary = c;
                                             break;
                                         }
                                     }
@@ -245,6 +262,7 @@ class Webshot extends CallableInstance {
             onready();
         }
         else {
+            this.wsUrl = wsUrl;
             this.connect(onready);
         }
     }

+ 1 - 0
package.json

@@ -36,6 +36,7 @@
     "koishi": "^3.10.0",
     "koishi-adapter-onebot": "^3.0.8",
     "log4js": "^6.3.0",
+    "playwright": "^1.9.1",
     "pngjs": "^5.0.0",
     "puppeteer": "^2.1.0",
     "read-all-stream": "^3.1.0",

+ 6 - 5
src/main.ts

@@ -60,7 +60,7 @@ const requiredFields = [
 ];
 
 const warningFields = [
-  'mirai_http_host', 'mirai_http_port', 'mirai_access_token',
+  'cq_http_host', 'cq_http_port', 'cq_access_token',
 ];
 
 const optionalFields = [
@@ -120,10 +120,10 @@ if (!config.resume_on_start) {
 }
 
 const qq = new QQBot({
-  access_token: config.mirai_access_token,
-  host: config.mirai_http_host,
-  port: config.mirai_http_port,
-  bot_id: config.mirai_bot_qq,
+  access_token: config.cq_access_token,
+  host: config.cq_http_host,
+  port: config.cq_http_port,
+  bot_id: config.cq_bot_qq,
   list: (c, a, cb) => list(c, a, cb, lock),
   sub: (c, a, cb) => sub(c, a, cb, lock, config.lockfile),
   unsub: (c, a, cb) => unsub(c, a, cb, lock, config.lockfile),
@@ -140,6 +140,7 @@ const worker = new Worker({
   bot: qq,
   webshotDelay: config.webshot_delay,
   mode: config.mode,
+  wsUrl: config.playwright_ws_spec_endpoint,
 });
 worker.launch();
 

+ 4 - 0
src/twitter.ts

@@ -19,6 +19,7 @@ interface IWorkerOption {
   accessTokenKey: string;
   accessTokenSecret: string;
   mode: number;
+  wsUrl: string;
 }
 
 export class ScreenNameNormalizer {
@@ -119,6 +120,7 @@ export default class {
   private webshotDelay: number;
   private webshot: Webshot;
   private mode: number;
+  private wsUrl: string;
 
   constructor(opt: IWorkerOption) {
     this.client = new Twitter({
@@ -133,6 +135,7 @@ export default class {
     this.bot = opt.bot;
     this.webshotDelay = opt.webshotDelay;
     this.mode = opt.mode;
+    this.wsUrl = opt.wsUrl;
     ScreenNameNormalizer._queryUser = this.queryUser;
     sendTweet = (id, receiver) => {
       this.getTweet(id, this.sendTweets(`tweet ${id}`, receiver))
@@ -178,6 +181,7 @@ export default class {
 
   public launch = () => {
     this.webshot = new Webshot(
+      this.wsUrl,
       this.mode,
       () => setTimeout(this.work, this.workInterval * 1000)
     );

+ 37 - 26
src/webshot.ts

@@ -5,8 +5,7 @@ import axios from 'axios';
 import * as CallableInstance from 'callable-instance';
 import { XmlEntities } from 'html-entities';
 import { PNG } from 'pngjs';
-import * as puppeteer from 'puppeteer';
-import { Browser } from 'puppeteer';
+import * as puppeteer from 'playwright';
 import * as sharp from 'sharp';
 
 import { getLogger } from './loggers';
@@ -31,23 +30,30 @@ const logger = getLogger('webshot');
 
 class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Promise<void>> {
 
-  private browser: Browser;
+  private browser: puppeteer.Browser;
   private mode: number;
+  private wsUrl: string;
 
-  constructor(mode: number, onready?: (...args) => void) {
+  constructor(wsUrl: string, mode: number, onready?: (...args) => void) {
     super('webshot');
     // tslint:disable-next-line: no-conditional-assignment
     // eslint-disable-next-line no-cond-assign
     if (this.mode = mode) {
       onready();
     } else {
+      this.wsUrl = wsUrl;
       this.connect(onready);
     }
   }
 
-  // use local Chromium
-  private connect = (onready: (...args) => void): Promise<void> =>
-    puppeteer.connect({browserURL: 'http://127.0.0.1:9222'})
+  private connect = (onready?: (...args) => void): Promise<void> =>
+    axios.get<{[key in 'chromium' | 'firefox' | 'webkit']?: string}>(this.wsUrl)
+      .then(res => {
+        logger.info(`received websocket endpoint: ${JSON.stringify(res.data)}`);
+        const browserType = Object.keys(res.data)[0] as keyof typeof res.data;
+        return (puppeteer[browserType] as puppeteer.BrowserType<puppeteer.Browser>)
+          .connect({wsEndpoint: res.data[browserType]});
+      })
       .then(browser => this.browser = browser)
       .then(() => {
         logger.info('launched puppeteer browser');
@@ -75,24 +81,29 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
       const width = 720;
       const zoomFactor = 2;
       logger.info(`shooting ${width}*${height} webshot for ${url}`);
-      this.browser.newPage()
+      this.browser.newPage({
+        bypassCSP: true,
+        deviceScaleFactor: zoomFactor,
+        locale: 'ja-JP',
+        timezoneId: 'Asia/Tokyo',
+        userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
+      })
         .then(page => {
           const startTime = new Date().getTime();
           const getTimerTime = () => new Date().getTime() - startTime;
           const getTimeout = () => Math.max(500, webshotDelay - getTimerTime());
-          page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
-            .then(() => page.setViewport({
-              width: width / zoomFactor,
-              height: height / zoomFactor,
-              isMobile: true,
-              deviceScaleFactor: zoomFactor,
-            }))
-            .then(() => page.setBypassCSP(true))
+          page.setViewportSize({
+            width: width / zoomFactor,
+            height: height / zoomFactor,
+          })
             .then(() => page.goto(url, {waitUntil: 'load', timeout: getTimeout()}))
             // hide header, "more options" button, like and retweet count
             .then(() => page.addStyleTag({
               content: 'header{display:none!important}path[d=\'M20.207 7.043a1 1 0 0 0-1.414 0L12 13.836 5.207 7.043a1 1 0 0 0-1.414 1.414l7.5 7.5a.996.996 0 0 0 1.414 0l7.5-7.5a1 1 0 0 0 0-1.414z\'],div[role=\'button\']{display: none;}',
             }))
+            .then(() => page.addStyleTag({
+              content: '*{font-family:-apple-system,".Helvetica Neue DeskInterface",Hiragino Sans,Hiragino Sans GB,sans-serif!important}',
+            }))
             // remove listeners
             .then(() => page.evaluate(() => {
               const poll = setInterval(() => {
@@ -153,7 +164,7 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
                 const idx = (x: number, y: number) => (this.width * y + x) << 2;
                 let boundary: number = null;
                 let x = zoomFactor * 2;
-                for (let y = 0; y < this.height; y++) {
+                for (let y = 0; y < this.height; y += zoomFactor) {
                   if (
                     this.data[idx(x, y)] !== 255 &&
                     this.data[idx(x, y)] === this.data[idx(x + zoomFactor * 10, y)]
@@ -176,7 +187,7 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
                   x = Math.floor(16 * zoomFactor);
                   let flag = false;
                   let cnt = 0;
-                  for (let y = this.height - 1; y >= 0; y--) {
+                  for (let y = this.height - 1 - zoomFactor; y >= 0; y -= zoomFactor) {
                     if ((this.data[idx(x, y)] === 255) === flag) {
                       cnt++;
                       flag = !flag;
@@ -190,16 +201,16 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
                     // if there are a "retweet" count and "like" count row, this will be the line above it
                     if (cnt === 4) {
                       const b = y + 1;
-                      if (this.height - boundary - (boundary - b) <= 1) {
+                      if (Math.abs(this.height - boundary - (boundary - b)) <= 3 * zoomFactor) {
                         boundary = b;
-                        //   }
-                        // }
+                      }
+                    }
 
-                        // // if "retweet" count and "like" count are two rows, this will be the line above the first
-                        // if (cnt === 6) {
-                        //   const c = y + 1;
-                        //   if (this.height - boundary - 2 * (boundary - c) <= 2) {
-                        //     boundary = c;
+                    // if "retweet" count and "like" count are two rows, this will be the line above the first
+                    if (cnt === 6) {
+                      const c = y + 1;
+                      if (Math.abs(this.height - boundary - 2 * (boundary - c)) <= 3 * zoomFactor) {
+                        boundary = c;
                         break;
                       }
                     }