我尝试通过向“https://{hostUrl}/topics/timings”发送带有 content-type、csrf token 和 user-agent 的 POST 请求来对该网站进行逆向工程。
这是请求体(json)的内容:
payload = {
"topic_id": topic_id,
"topic_time": post_count * 60000,
"timings": timings
}
它返回的状态码是 200,但已读历史记录在 https://{hostUrl}/u/USERNAME/activity/read 处从未改变。
我尝试查看这篇文章,但帮助不大:
Hello,
I’m pulling posts through the discourse API. I’m using python for that. The request calls the following code (which makes a GET request essentially) and returns the .json similar to this post
def topic_by_id(self, topic_id, **kwargs):
return self._get("/t/{0}.json".format(topic_id), **kwargs)
The posts returned have a 'read' flag. The posts I send are read=True, but for the posts I receive they are all marked as read=False, unless I actively log in to Discourse and read the…
以下是大部分代码:
def get_csrf(session):
r = session.get(f"https://{hostUrl}/session/csrf.json")
if r.status_code != 200:
raise RuntimeError("获取 CSRF 失败")
data = r.json()
if "csrf" not in data:
raise RuntimeError("响应中没有 CSRF")
return data["csrf"]
def load_topics(session, page):
print(f"[主题] 第 {page} 页")
r = session.get(
f"https://{hostUrl}/latest.json?page={page}"
)
if r.status_code != 200:
return []
data = r.json()
return [
{
"id": t["id"],
"posts_count": t["posts_count"]
}
for t in data["topic_list"]["topics"]
]
def mark_post_as_read(session, topic_id, post_count):
url = f"https://{hostUrl}/topics/timings"
timings = {
str(i): 60000
for i in range(1, post_count + 1)
}
payload = {
"topic_id": topic_id,
"topic_time": post_count * 60000,
"timings": timings
}
csrf = get_csrf(session)
r = session.post(
url,
json=payload,
headers={
"X-CSRF-Token": csrf,
"User-Agent": "Mozilla/5.0",
"Content-Type": "application/json"
}
)
print(f"[已读] {topic_id} → {r.status_code}")
if r.status_code != 200:
print(r.text[:300])
def tab_worker(session):
page = 1
while True:
topics = load_topics(session, page)
if not topics:
break
for t in topics:
mark_post_as_read(
session,
t["id"],
t["posts_count"]
)
time.sleep(0.4)
page += 1
Canapin
(Coin-coin le Canapin)
2026 年2 月 3 日 23:54
3
kittenwater:
"timings": timings
如果以扁平的 "timings[post_number]": [duration_in_ms] 形式发送,可以吗?它能工作吗?
2 个赞
它似乎不能完全奏效。
这是我修改后的代码片段。
def mark_post_as_read(session, topic_id, post_count):
url = f"https://{hostUrl}/topics/timings"
payload = {
"topic_id": topic_id,
"topic_time": post_count * 60000
}
for i in range(1, post_count):
payload[f"timings[{i}]"] = 60000
csrf = get_csrf(session)
r = session.post(
url,
json=payload,
headers={
"X-CSRF-Token": csrf,
"User-Agent": "Mozilla/5.0",
"Content-Type": "application/json"
}
)
print(f"[Read] {topic_id} → {r.status_code}")
if r.status_code != 200:
print(r.text[:300])
它仍然返回 200,但没有更新。
谢谢
Canapin
(Coin-coin le Canapin)
2026 年2 月 5 日 13:08
5
代码看起来没问题,我不确定问题出在哪里。
我的直觉告诉我,我们只是在某个地方遗漏了显而易见的东西
Canapin
(Coin-coin le Canapin)
2026 年2 月 5 日 23:01
7
This works:
def load_topics(session, page):
print(f"[Topics] 正在加载第 {page} 页")
r = session.get(f"https://{hostUrl}/latest.json?page={page}")
if r.status_code != 200:
return []
return [{"id": t["id"], "posts_count": t["posts_count"]} for t in r.json()["topic_list"]["topics"]]
timings = {
str(i): 60000
for i in range(1, post_count + 1)
}
payload = {
"topic_id": topic_id,
"topic_time": post_count * 60000,
"timings": timings
}
# 使用 json=payload 发送 application/json
r = session.post(url, json=payload, headers = {
"X-CSRF-Token": csrf,
"User-Agent": "Mozilla/5.0",
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json"
}
)
1 个赞
Canapin
(Coin-coin le Canapin)
2026 年2 月 6 日 11:02
10
我不会对这些统计数据是由常规的后台作业出于性能原因而更新感到惊讶。
你用脚本更新时间戳的用例是什么?
我可以百分之百确定这个说法是正确 的!
帖子阅读数更新了好几次,但主题阅读数没有。它们的间隔时间不同吗?已经大约 20 小时了,帖子阅读数在增加,但主题阅读数没有增加。
我只是想尝试逆向工程这些端点!这很酷。
我想我应该再等一会儿,然后再回来看看这些值是否发生了变化。
Canapin
(Coin-coin le Canapin)
2026 年2 月 6 日 21:12
12
kittenwater:
我想我应该等一会儿再回来看看这些值是否已更改
如果你能找出是哪个 job,你可以在 /sidekiq/scheduler 中手动触发 sidekiq job
Canapin
(Coin-coin le Canapin)
2026 年2 月 6 日 21:25
13
也许是 Jobs::DirectoryRefreshDaily。
Moin
2026 年2 月 6 日 21:28
14
kittenwater:
我说的就是这两个!
您在用户目录的 /u?period=daily 或每周的视图中是否看到了相同的内容?在那里,您可以看到顶部数字的更新时间。
我认为“今天”的数字每小时更新一次,而其他时间段每天只更新一次。
1 个赞
@Canapin ,我不是该网站的所有者。如果我仍然可以仅以普通用户身份登录并触发它,请告诉我具体方法。
@Moin
我正在使用的网站禁用了该功能,并且总是返回“此处将显示一个显示其活动的社区成员列表。目前列表为空,因为您的社区还很新!”
Canapin
(Coin-coin le Canapin)
2026 年2 月 7 日 10:45
16
在他的情况下,你不能。作为普通用户不利于对 API 进行逆向工程。
如果你可以,尝试本地开发安装或在廉价的 VPS(一个 3-4 美元的服务器就可以了)上进行生产安装,因为 Discourse 不再需要主机名或 SMTP。
1 个赞
嘿,@Canapin ,感谢您持续的帮助。
我该如何做到这一点?它目前显示已读 22 个主题 和已读 260 万条帖子