Nuggets community in September 23 opened the “breaking circle action”, the activity content is in September 23 – September 30 period, crack 3 circles every day (crack method: release boiling point in the circle), according to the number of days to win peripheral prizes.

On the first day of participating in the activity, I found a pain point, and the following points should be paid attention to:

  1. There are 6 circles that are not participating in this event, so keep these 6 circles in mind and be careful
  2. A total of 30+ circles, how to avoid repeated cracking

Where there is a pain point, there is a clever way. A student planned three circles for daily cracking through a table and solved the above problems easily with existing tools.

I wanted some visual feedback and motivation when participating in activities, so I came up with the idea of writing an “activity aid tool”. Follow current nuggets activities and provide status tracking. Sounds like a pretty good pie to start with in Operation Loophole.

Oil monkey script development

Oil monkey script installation threshold is low, is the most suitable carrier.

The oil Monkey plugin is available on all major browser markets. The only stumbling block is access to the Chrome market. For now, it looks like you can either use a third-party installation source (which is less secure) or move to Edge at 🧐

The ideal development process is to edit it in VSCode and save it to take effect automatically.

Oilmonkey provides the @require field to load external resources, such as loading and running jquery

/ / @ the require https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js
Copy the code

You can use this feature to run native JS files. Note when configuring:

  1. @requireAfter fill in the file full path tofile://Agreement at the beginning
  2. Check on the plug-in Settings pageAllow access to file URLs

The source code is for uploading and sharing, and the script is for development. Therefore, the important information in the UserScript section of the two versions needs to be consistent to avoid running differences between the development and shared versions. You can also use this feature to differentiate environments so that you don’t have to overwrite scripts when switching environments.

Shared version Settings download and update links

// ==UserScript==
// @name Juejin Activities Enhancer
// @name: zh-cn gold digging activities auxiliary tools
// @namespace https://github.com/curly210102/UserScripts
/ / @ version 0.1.6.3
// @description Enhances Juejin activities
// @author curly brackets
// @match https://juejin.cn/*
// @license MIT License
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// @connect juejin.cn
// @supportURL https://github.com/curly210102/UserScripts/issues
// @updateURL https://github.com/curly210102/UserScripts/raw/main/Juejin_Enhancer/Juejin_activities.user.js
// @downloadURL https://github.com/curly210102/UserScripts/raw/main/Juejin_Enhancer/Juejin_activities.user.js
// ==/UserScript==
Copy the code

The development version @name follows a Dev

// ==UserScript==
// @name Juejin Activities Enhancer Dev
// @name: zh-cn gold digging activities auxiliary tools
// @namespace https://github.com/curly210102/UserScripts
/ / @ version 0.1.6.3
// @description Enhances Juejin activities
// @author curly brackets
// @match https://juejin.cn/*
// @license MIT License
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// @connect juejin.cn
// @require file:///Users/curly/Workspace/opensource/UserScripts/Juejin_Enhancer/Juejin_activities.user.js
// ==/UserScript==
Copy the code

To add a little more API tips and completion to VSCode, install the VisualStudio Marketplace – Tampermonkey Snippets plug-in.

That makes development a lot easier.

Rules of the comb

Extract key points from official rules.

Broken circle needs to meet:

  1. Not in the six excluded circles
  2. Not in the last few days
  3. Boiling point content approved

One day to achieve the standard needs to meet:

  • The number of circles cracked in a day is greater than or equal to 3

The final award will be awarded according to the number of days reaching the standard and the number of boiling points released

Effect of the design

Once the rules are clear, start imagining the effects of assistive tools.

Activity statistics

Display activity statistics on personal home page and boiling point page, track progress achieved and corresponding awards;

According to the odd characteristics of the first to third prizes, the award correspondence can be described as

["Lucky prize"."Third prize"."Second prize"."First Prize"."Full Attendance Award"][
    efficientDays >= 8 ? 4 : Math.floor((efficientDays - 1) / 2)?? (efficientTopicCount >1 ? "Lucky prize" : "No")
Copy the code

Activity dependent data statistics, there are: standard days efficientDays, hacking community efficientTopicCount, today todayEfficientTopicTitles crack circle.

Circle selection menu

In the circle selection menu, mark the circle that does not participate in the activity, display the cracking status and audit status of the circle.

From top to bottom, respectively is not cracked, cracked, do not participate in three states

Because of the delay between publishing and auditing, you need to distinguish between circles that have been created but not yet audited.

Has been released without review, gray circle bottom

The circle selection menu relies on the following data: blockTopics that do not participate in the activity, efficientTopics of the cracked circle, the number of boiling points of the cracked circle and the review status of the circle.

Design a data structure based on the above analysis

interface IStates {
    todayEfficientTopicTitles: string[].// Circle of the day
    efficientDays: number.// Days to reach the target
    efficientTopics: {  // Break circles: paid points, audit status (approved or waiting for audit)
        [title: string] : {count: number.verify: boolean}}},const BLOCKTOPICS = [
    "A hole in the tree."."Gold Digging Blind date"."Feedback & Suggestions"."Boiling point welfare"."Gold Digger official"."Fishing at work."
];
Copy the code

Data acquisition and processing

Firstly, the boiling points released from September 23 to September 30 were collected to do some basic screening and grouping processing.

Query interfaces using the boiling point list, 24 (8 x 3) at a time, arranged in chronological order from near to far.

Traversing the data, when the creation time of the boiling point earlier than September 23 jumped out; When the last boiling point is created later than September 23, set cursor to continue the request. To avoid nested pass callback functions, use Promise.

After collecting data, do basic validation and filter based on time range and BLOCKTOPICS.

In order to facilitate the calculation of the number of days up to the standard, here is grouped by the number of active days, using the array index storage. The resulting data set looks like this:

type IDailyTopics = ArrayThe < {title: string.verified: boolean} >Copy the code

Full code implementation

const startTimeStamp = 1632326400000; / / 9/23 00:00
const endTimeStamp = 1633017600000; / / 10/01 00:00

function requestShortMsgTopic (cursor = "0", dailyTopics = []) {
    return new Promise((resolve, reject) = > {
        GM_xmlhttpRequest({
            method: "POST".url: "https://api.juejin.cn/content_api/v1/short_msg/query_list".data: JSON.stringify({
                sort_type: 4.cursor: cursor,
                limit: 24.user_id: userId,
            }),
            headers: {
                "User-agent": window.navigator.userAgent,
                "content-type": "application/json",},onload: function ({ status, response }) {
                if(status ! = =200) {
                    return reject();
                }
                const responseData = JSON.parse(response);
                const { data, cursor, has_more } = responseData;
                let lastPublishTime = Infinity;
                for (const msg of data) {
                    // ctime Creation time, mtime modification time, rtime manual review time
                    const publishTime = msg_Info.ctime * 1000;
                    lastPublishTime = publishTime;
                    if (publishTime < startTimeStamp) {
                        break;
                    }

                    // Group by active days
                    if( publishTime > startTimeStamp && publishTime < endTimeStamp && ! BLOCKTOPICS.includes(topic.title) ) {const day = Math.floor(
                            (publishTime - startTimeStamp) / 86400000
                        );
                        if(! dailyTopics[day]) { dailyTopics[day] = []; }Msg_info. status === 2&&msg_info. verify_status === 1 '
                        Audit has three states: wait, pass, and fail
                        dailyTopics[day].push({
                            title: topic.title,
                            // wait: 0, pass: 1, fail: 2
                            verified:
                            msg_Info.status === 1 ||
                            msg_Info.verify_status === 0
                                ? 0
                                : msg_Info.status === 2 &&
                                msg_Info.verify_status === 1
                                ? 1
                                : 2}); }}// Still have data, continue to request
                if (lastPublishTime > startTimeStamp && has_more) {
                    resolve(requestShortMsgTopic(cursor, dailyTopics));
                } else{ resolve(dailyTopics); }}})})}Copy the code

After taking the grouped data, the final data structure is assembled through further verification and the number of days to reach the target.

In order to show the circles that are awaiting approval, the restrictions on broken circles are relaxed to include both those that have been approved and those that are awaiting approval. Make the distinction in the render presentation layer.

Set a Set to host cracked circles for repeated verification (allEfficientTopicTitles in code). The number of boiling point and the audit situation open another set record, in the code topicCountAndVerified.


function filterEfficientData (dailyTopics) {
    const allEfficientTopicTitles = new Set(a);const topicCountAndVerified = {};
    const todayIndex = Math.floor(
      (new Date().valueOf() - startTimeStamp) / 86400000
    );
    const todayEfficientTopicTitles = [];
    let efficientDays = 0;
    // Process by day
    dailyTopics.forEach((topics, index) = > {
      // Get circles cracked in a day
      const dailyEfficientTopicTitles = new Set(
        topics
          .filter(({ title, verified }) = > {
            // Broken circle: not cracked + approved or waiting for approval
            return! allEfficientTopicTitles.has(title) && verified ! = =2;
          })
          .map(({ title }) = > title)
      );
      // Update the number of days to reach the target
      if (dailyEfficientTopicTitles.size >= 3) {
        efficientDays++;
      }
      // Record today's break number
      if(index === todayIndex) { todayEfficientTopicTitles.push(... dailyEfficientTopicTitles); }// Update the broken loop set
      dailyEfficientTopicTitles.forEach((t) = > allEfficientTopicTitles.add(t));
      // Record the number of posts broken
      topics.map(({ title, verified }) = > {
        if(! topicCountAndVerified[title]) { topicCountAndVerified[title] = {count: 1,
            verified,
          };
        } else {
          topicCountAndVerified[title]["count"] + +; topicCountAndVerified[title]["verified"] ||= (verified === 1); }}); });// Assemble data
    setStates({
      todayEfficientTopicTitles,
      efficientDays,
      efficientTopics: Object.fromEntries(
        [...allEfficientTopicTitles].map((title) = > {
          return[title, topicCountAndVerified[title]]; })}); }Copy the code

Apply colours to a drawing

The Oilmonkey script is executed when onDOMContentLoaded, and the site cannot get the target node before completing the waterflood rendering. Rendering at the initial stage of a page can be addressed using setTimeout delays (crude but practical).

But DOM updates at run time introduce a listening mechanism. In React/Vue built applications, data updates may trigger DOM destruction, update, and reconstruction. Plug-ins cannot invade the application and can only listen for DOM changes outside the application. Use MutationObserver to do this.

MutationObserver is an interface set up by the DOM3 Events specification to observe changes in the DOM tree, including property changes (attribute lists can be specified), child node additions or deletions, and text content changes. There are no compatibility issues.

Back to the script, there are two places in the script that need to perform DOM monitoring, namely: circle selection list for searching or Tab to trigger DOM changes, and the boiling point publishing popover triggered by the entry in the upper right corner. Use Tab switching as an example of MutationObserver usage

    // 1. Obtain the target node
    const topicPanel = containerEl.querySelector(
      ".topicwrapper .new_topic_picker"
    );
    if(! topicPanel) {return;
    }

    // 2. Construct an observer and set up the processing logic
    const observer = new MutationObserver(function (mutations) {
      mutations.forEach(({ type, addedNodes }) = > {
          // Listen for new child nodes in the DOM tree
        if (type === "childList" && addedNodes.length) {
          // Check the child node
          addedNodes.forEach((itemEl) = > {
            if(! itemEl) {return;
            }
            if(itemEl? .classList? .contains("contents")) {
              // If the child contains the.contents style class, the entire Panel has been replaced and needs to be rerendered as a whole
              renderWholeContent(itemEl);
            } else {
              // Add a new node to the Panel to render a single itemrenderItem(itemEl); }}); }}); });// 3. Start to observe the target node and specify what to observe
    // childList: observe the insertion or removal of child nodes
    // subtree: set the observation range, whether to go into the descendant node
    observer.observe(topicPanel, {
      childList: true.subtree: true});Copy the code

To optimize the

Once the basic functionality is complete, consider the details.

Run time

The oil Monkey script only runs after the page is loaded, but the gold digging master uses single-page routing based on Nuxt. The script will not be rerun when the route is switched, and additional route listening is required to drive the script.

The popState event is triggered only when the browser is rolled back. This is done by overriding history.pushState and history.replaceState to implement full route listening. InitByRouter is used as the logical startup entry.

const _historyPushState = history.pushState;
  const _historyReplaceState = history.replaceState;
  history.pushState = function () {
    _historyPushState.apply(history, arguments);
    initByRouter();
  };
  history.replaceState = function () {
    _historyReplaceState.apply(history, arguments);
    initByRouter();
  };
  window.addEventListener("popstate".function () {
    initByRouter();
  });
Copy the code

Timing of data requests

There’s no need to request the API every time you go to the gold mining page, only when you need it:

  1. Boiling point home page
  2. Personal home page
  3. Invokes release boiling point popover

Do some routing control in initByRouter, enter the boiling point page and personal home page to follow the rendering logic, in the page internal jump do not have to repeat the request.

let currentRouterPathname = "";
function initByRouter () {
    const prevRouterPathname = currentRouterPathname;
    currentRouterPathname = document.location.pathname;

    const pagePinsRegexp = /^\/pins(? : \ | $) /;
    if(pagePinsRegexp.test(currentRouterPathname) && ! pagePinsRegexp.test(prevRouterPathname)) { renderInPagePins();return;
    }
    
    const pageProfileRegexp = new RegExp(`^\\/user\\/${userId}(? : \ \ / ` | $));
    if( pageProfileRegexp.test( currentRouterPathname ) && ! pageProfileRegexp.test(prevRouterPathname) ) { renderInPageProfile();return; }}Copy the code

To this, finally realized a relatively complete small tool.

Afterword.

In the process of writing this article, I have combed the logic again and found some points that can be improved, which is also an unexpected harvest.

While JavaScript is flexible and fast to implement, the need for Babel, ESLint, and TypeScript is very obvious when writing code.

Activity AIDS This pie has been lost, can also be hard to eat. If you are also interested, welcome to join us.

Boiling point discussion area

Script installation address – Gitee

Source code address – Gitee

Script installation address – GitHub

Source address – GitHub