Discourse 源码分析 —— 阅读时间

研究背景

我们在 个人资料总结统计信息 中可以看到我们的阅读时间。

那 Discourse 是如何进行用户阅读时间的统计的?

研究方法

  1. 控制台看网络请求。
  2. 阅读 Discourse 源码。

查看网络请求

阅读帖子过程中浏览器会向 /topics/timings 路由发送请求,且无返回。

阅读 Discourse 源码

前端代码

screen-track.js

在 185 行中我们发现向后端 /topics/timings 路由发送请求的代码。

      await ajax("/topics/timings", {
        data,
        type: "POST",
        headers: {
          "X-SILENCE-LOGGER": "true",
          "Discourse-Background": "true",
        },
      });

通过本文件的其他代码发现有许多控制逻辑:

后端请求

  1. 跟踪阅读时间: 用户在帖子上的停留时间通过 tick() 方法周期性地更新。这个方法计算自上次"tick"以来经过的时间,并将这个时间累加到对应帖子的阅读时间上。
  2. 记录滚动事件: 当用户滚动页面时,scrolled() 方法会被触发,更新 _lastScrolled 时间戳,这样可以防止有些人挂机过度计算阅读时间。
  3. 数据合并和发送: flush() 方法负责将当前会话的阅读时间聚合并准备发送给服务器。consolidateTimings() 方法用于合并当前话题的阅读时间数据。sendNextConsolidatedTiming() 方法通过 AJAX 请求将累积的阅读时间数据发送到服务器。

计时规则

  1. 暂停计时: 如果用户一段时间内没有滚动页面(由 PAUSE_UNLESS_SCROLLED 定义,为3分钟),tick() 方法将停止累加阅读时间,直到下一次用户滚动。
  2. 最大跟踪时间: 每个帖子的跟踪时间有上限(由 MAX_TRACKING_TIME 定义,为6分钟)。所以用户每次(重新进入帖子会重新计算)在这个帖子停留时间过长,超过这个时间的部分不会被计入总阅读时间。
  3. 匿名用户限制: 系统通过 ANON_MAX_TOPIC_IDS 限制了匿名用户可以跟踪的不同话题ID数量(最多5个)。
  4. 请求失败处理: 如果数据发送失败(基于 ALLOWED_AJAX_FAILURES 定义的允许的失败状态),会根据 AJAX_FAILURE_DELAYS 数组定义的延迟策略重新尝试发送。

后端代码

topics_controller.rb

在 946 行中我们发现前端向后端发送的请求通过 PostTiming 类进行处理

  def timings
    allowed_params = topic_params

    topic_id = allowed_params[:topic_id].to_i
    topic_time = allowed_params[:topic_time].to_i
    timings = allowed_params[:timings].to_h || {}

    # ensure we capture current user for the block
    user = current_user

    hijack do
      PostTiming.process_timings(
        user,
        topic_id,
        topic_time,
        timings.map { |post_number, t| [post_number.to_i, t.to_i] },
        mobile: view_context.mobile_view?,
      )
      render body: nil
    end
  end

post_timing.rb

在这个文件中,我们可以看到如何将阅读时间存储到数据库中的

pretend_read 方法

此方法用于“假装”用户已经阅读了特定帖子。通过执行一条 SQL 插入语句来实现,这条语句只在目标帖子尚未被记录为由特定用户读取时才插入新的 post_timings 记录。

比如当用户直接回复一个他们实际上没有阅读的帖子时,需要将此帖子标记为已读。

record_new_timing

此方法用于记录新的阅读时间。通过执行一条 SQL 插入语句来实现,如果新记录与现有记录冲突(基于 topic_idpost_numberuser_id 的唯一性约束),则什么都不做。插入成功的话还会更新相关帖子的 reads 计数器和用户的 posts_read_count(除是私人消息以外)。

record_timing

此方法用于更新已存在的 post_timings 记录,增加 msecs 值。如果指定的记录不存在(即没有为特定用户、话题和帖子编号找到匹配的记录),则调用 record_new_timing 来创建新记录。

destroy_last_fordestroy_for

此方法用于删除用户对特定话题或多个话题的阅读时间记录。在删除记录后还会更新相关的 TopicUserUserStat 记录,以保持数据的一致性。

process_timings

此方法用于处理一批阅读时间数据。它首先更新用户的总阅读时间,然后处理每个帖子的阅读时间。这里会限制每个帖子的最大阅读时间(见以下代码),并尝试更新现有的 post_timings 记录。如果记录不存在,则创建新的记录。此方法还用于处理帖子阅读通知的发送和用户最后阅读位置的更新。

    max_time_per_post = ((Time.now - current_user.created_at) * 1000.0)
    max_time_per_post = MAX_READ_TIME_PER_BATCH if max_time_per_post > MAX_READ_TIME_PER_BATCH

结论

每次循环请求后端 /topics/timings 只能填 60000 以下。

21 个赞

万一研究出什么后门?

1 个赞

后门皇会提交 pr

22 个赞

什么时候研究出来帮我刷时间

???

2 个赞

加油,找个后门

4 个赞

不是已经研究出来了才发的么

21 个赞

看完开始动坏心思了

26 个赞

那帮我直接拉满吧 :smile:

找个后门吧,然后人人4级

水王,你变了 :melting_face:

哪里变了,都是为了我们的积分大业,不忘初心

21 个赞

赞 怪不得我挂机都不涨的 哈哈哈哈哈 :rofl:

原来不能挂机,怪不得我的阅读时间才这么短

11 个赞

:rofl:白挂了

21 个赞

可以挂机,得上手段

21 个赞

厉害了,我的佬

佬来点手段

10 个赞

刚刚测了一下,貌似可以用fetch() post一下。 :face_with_peeking_eye:

26 个赞

别说原理,直接给我上插件或者脚本吧(一本正经提