wechat-moments

August 7, 2026 · View on GitHub

Rank and summarize your own WeChat Moments (朋友圈) feed, so the post that matters stops being buried between a coffee photo and a course ad.

This tool does not decrypt anything and never handles keys. It reads a plaintext database that a separate export tool has already produced, then does the part those tools do not: work out which posts are worth your attention.

Everything stays on your machine. Nothing is uploaded, posted, liked, commented, or sent.

Companion to OpenClaudia.

You need an exporter first

Getting a readable copy of your own WeChat data is a separate job with several existing tools. Pick one, run it, and point this at the result:

They are not affiliated with this project and we do not vouch for them. Read their code before running it.

Requirements

Verified onNotes
Exporta plaintext sns.db from one of the tools aboveMust include the Moments store; contacts and messages unlock relationship ranking
WeChat for macOS3.8.7 (build 28249)Only needed for sync and capture.py, which drive the running app
macOS26.5 (25F71)
Python3.12Standard library only for sns.py and report.py
pyobjc-framework-Quartz11.1capture.py only — pip install pyobjc-framework-Quartz

Point it at your export:

export WECHAT_EXPORT=~/path/to/decrypted     # expects Sns/, Contact/, Message/ inside
# or per-file:
export WECHAT_SNS_DB=/path/to/sns.db
export WECHAT_CONTACT_DB=/path/to/contacts.db
export WECHAT_MSG_DIR=/path/to/messages/

Use

python3 scripts/sns.py list 20      # recent posts, one line each
python3 scripts/sns.py json         # structured records, for ranking
python3 scripts/sns.py mine         # your own posts + who engaged
python3 scripts/sns.py labels       # contact-label clusters + members
python3 scripts/sns.py --check      # self-check the parser
python3 scripts/sns.py sync         # make WeChat fetch new posts (then re-export)
python3 scripts/capture.py 12       # screenshot the Moments window while scrolling
python3 scripts/report.py picks.json # render an HTML digest

Each record:

id, time, name, remark, nickname, wxid, labels[], text, images, ad, via,
mine, sent, recv, days_since_talked, likes, comments[]

Read this before you use it

  • Moments do not sync in the background. Verified over an hour with WeChat running and chat messages arriving normally: zero new posts. The client fetches the timeline only while the Moments window is open, which is what sync opens. Without it your export only ever contains what you already scrolled past. WeChat also debounces — a second sync within a minute is a genuine no-op.
  • Your Mac has to stay awake for the feed to stay complete. Any stretch where the machine slept or WeChat was quit is permanently lost; the client will not backfill it and neither can this. A laptop that sleeps overnight will have holes.
  • WeChat must be running and logged in for sync and capture.py.
  • Version-sensitive, and it fails quietly. Everything here depends on an undocumented layout that changes between client releases. The parser is deliberately tolerant, so a mismatch usually shows up as missing text or missing author names rather than an error. Run sns.py --check after any WeChat update and treat a sudden drop in parsed posts as breakage, not as a quiet feed.
  • Tencent can break this at any time, deliberately or not, possibly in a routine update.
  • Automating a client can be noticed. sync and capture.py drive the real app — opening and scrolling windows. WeChat may detect unusual local activity, may warn you, and in principle may treat it as a terms-of-service problem for your account. Nobody here can promise otherwise. Weigh that before running it on a schedule.
  • No warranty of any kind. MIT, as-is. Nothing about correctness, completeness, continued operation, or the safety of your account is guaranteed. You run it at your own risk.

Images

There are none to embed. The image CDN rejects requests that do not come from the real client, and WeChat's local image cache is not addressable from the post record. capture.py screenshots the Moments window while scrolling, which is the only route to the pictures; report.py appends those frames if present.

Two things about that automation are load-bearing and documented in the code: it never activates WeChat (which dismisses the very window being captured), and it resolves the window by name every frame rather than by index (which once meant screenshotting a chat window full of private conversations).

Ranking, and why the obvious signals are wrong

The actual point of this repo. These failures generalise well past WeChat.

Total message count is not closeness — direction is. A marketer who sends dozens of pitches and never gets a reply scores identically to a real friend. sent (messages you sent) is the signal, because a reply is a choice and an inbound blast is not. Hence the sent / recv split rather than one count.

days_since_talked earns its own read. High sent with high days is a lapsed close friend, and a life event from one of those is the most actionable thing a digest can surface.

A close friend's small post usually beats a stranger's big news, because only one of them is a relationship you can act on. Ranking on content alone inverts this every time — in testing, about a third of a day's posts came from contacts the account holder had never exchanged a single message with, and several had been promoted to the top on content alone.

Insight vs. announcement. Real information has a number you could check, a mechanism, a cost the author actually paid, or a changed mind. Adjectives, a milestone with no mechanism, an invitation, or a takeaway generic enough to predate the event are announcements. Prestige is not evidence.

Promotion wears a personal voice. A discount code, a signup link, an invite-only pitch: the poster is selling, however casual the phrasing.

Urgency is a modifier, never a reason. A dated section at the top of a digest promotes whatever has a deadline, and deadlines are what promotions have. An expiring ad is still an ad.

Likes are not a ranking signal. The engagement table is a personal notification feed, not a per-post count, so another person's post only ever shows the slice that reached you.

Contact labels are numeric ids. The id→name map is not stored locally. Run sns.py labels, name each cluster from its members, and save the map to labels.json beside the scripts (gitignored — it is your data):

{"7": "family", "23": "university", "41": "clients"}

License

MIT