|
1 | | -# Reddit Chat Go |
| 1 | +# Reddit chat protocol client |
2 | 2 |
|
3 | | -Go wrapper for Reddit's Matrix-backed chat service using mautrix-go. |
| 3 | +This package implements the Reddit endpoints used by the bridge. It uses |
| 4 | +`maunium.net/go/mautrix` for Reddit's Matrix-shaped chat protocol; bridgev2 owns |
| 5 | +local message delivery and mappings. |
4 | 6 |
|
5 | | -Note: the GitHub project is `github.com/mautrix/go`, but its `go.mod` declares the canonical Go import path as `maunium.net/go/mautrix`. Importing `github.com/mautrix/go` directly fails module path validation, so this library uses the canonical mautrix-go import path. |
| 7 | +## Authentication |
6 | 8 |
|
7 | | -## Auth |
| 9 | +The connector drives the native operations through bridgev2 login steps: |
8 | 10 |
|
9 | | -Reddit chat stores Matrix credentials in `https://www.reddit.com` local storage after the chat app loads. The same credentials can also be minted directly from a logged-in Reddit web session by POSTing to `/svc/shreddit/token` with the `csrf_token` cookie. |
| 11 | +1. Create a `RedditSession` and call `PrepareLogin` with the username. |
| 12 | +2. Present `CaptchaRequest(CaptchaStepPassword)` in the framework's hidden |
| 13 | + verification webview, then pass its result to `SubmitPassword`. |
| 14 | +3. If Reddit requires two-factor authentication, collect the user's code and |
| 15 | + a fresh `CaptchaRequest(CaptchaStepOTP)`, then call `SubmitOTP` on the same |
| 16 | + session. Do not repeat the password login. |
| 17 | +4. Call `CredentialsFromRedditSession` to mint chat credentials. The connector |
| 18 | + persists the session cookies for renewal and validates the account on refresh. |
10 | 19 |
|
11 | | -Supported extraction paths: |
| 20 | +Each operation accepts a context and performs only that step's HTTP requests. |
| 21 | +There is no background login worker or browser-debug connection. OTP codes are |
| 22 | +provided by the user; the bridge does not accept authenticator secrets. |
12 | 23 |
|
13 | | -- `LoginReddit(ctx, opts)` implements Reddit's current web login flow and returns Reddit session cookies plus Matrix credentials. |
14 | | -- `NewFromRedditLogin(ctx, opts)` logs in, mints Matrix credentials, and returns a ready chat client. |
15 | | -- `CredentialsFromRedditSession(ctx, session)` POSTs `/svc/shreddit/token`, decodes the chat JWT into a Matrix user ID, calls Matrix `whoami` for the device ID, and returns `Credentials`. |
16 | | -- `RedditSessionFromPlaywrightStorageState(path)` imports Reddit session cookies from a Playwright storage-state JSON file without using the old chat localStorage credentials. |
17 | | -- `CredentialsFromPlaywrightStorageState(path)` reads a Playwright storage-state JSON file. |
18 | | -- `CredentialsFromChromeDebugURL(ctx, url)` attaches to a live Chrome/Chromium debug endpoint, opens `https://www.reddit.com/chat/`, and reads the same local storage keys through CDP. |
19 | | -- `SeedChromeFromPlaywrightStorageState(ctx, url, path)` seeds a live Chrome/Chromium debug endpoint from a Playwright storage-state JSON file, then the CDP extractor can read from the real browser session. |
| 24 | +## Chat protocol |
20 | 25 |
|
21 | | -Chrome debug endpoints may be passed as `http://127.0.0.1:9222` or a browser websocket URL. |
| 26 | +Reddit uses `matrix.redditspace.com`, native `t2_` user IDs, opaque event IDs |
| 27 | +and provider pagination cursors. Its request previews also appear in |
| 28 | +`rooms.peek`. Media downloads use the native `/media/v3/download` endpoint; |
| 29 | +thread pages use Reddit's sequenced relations API. These differences are |
| 30 | +handled here and by the connector, without overriding bridgev2 delivery. |
22 | 31 |
|
23 | | -Login API notes: |
24 | | - |
25 | | -- Reddit serves a deterministic JavaScript verification page before the login UI; the library follows that page transition. |
26 | | -- The username/password step is `POST /svc/shreddit/account/login/check_is_oidc_required`, then `POST /svc/shreddit/account/login`. |
27 | | -- Accounts with app-based 2FA receive HTTP 202 from the password step; the OTP step is `POST /svc/shreddit/account/login/otp`. |
28 | | -- Reddit requires a reCAPTCHA Enterprise token for password and OTP submits. The library accepts a `CaptchaTokenProvider` callback and does not include CAPTCHA solving, media extraction, or bypass logic. |
29 | | -- Reddit's current login flow does not POST an image/audio CAPTCHA answer to Reddit. Google reCAPTCHA runs in the browser and returns an opaque, short-lived `recaptcha_token`; that token is the value Reddit receives. |
30 | | -- If no provider is configured, `LoginReddit` returns `CaptchaRequiredError` with the site key, action, and page URL needed by a caller-managed CAPTCHA flow. |
31 | | -- `StaticCaptchaTokenProvider(tokens...)` can be used when the caller already has one or more fresh reCAPTCHA tokens. Accounts with 2FA need two tokens: one for the password step and one for the OTP step. |
32 | | -- `CaptchaTokenProviderFromChromeDebugURL(url)` uses a real Chrome/Chromium debug session, opens the Reddit login page, runs Google reCAPTCHA Enterprise on Reddit's origin, and returns the resulting token. If Google presents an interactive challenge, the user handles it in that browser. |
33 | | -- `CaptchaRequest.Step` is `password` or `otp`, so UI code can show why a token is being requested. |
34 | | -- For an embedded WebView provider, load `CaptchaRequest.PageURL`, then evaluate `CaptchaRequest.EnterpriseExecuteJavaScript()` on that page and return the resulting token. Username, password, and 2FA do not need to be entered in the WebView. |
35 | | -- TOTP generation is built in through `GenerateTOTP` and `RedditLoginOptions.TOTPSecret`. |
36 | | - |
37 | | -Minimal login shape: |
38 | | - |
39 | | -```go |
40 | | -client, session, err := redditchat.NewFromRedditLogin(ctx, redditchat.RedditLoginOptions{ |
41 | | - Username: username, |
42 | | - Password: password, |
43 | | - TOTPSecret: totpSecret, |
44 | | - CaptchaTokenProvider: redditchat.CaptchaTokenProviderFromChromeDebugURL("http://127.0.0.1:9222"), |
45 | | -}) |
46 | | -_ = client |
47 | | -_ = session |
48 | | -``` |
49 | | - |
50 | | -The Chrome-backed provider needs a running browser with remote debugging enabled, for example: |
51 | | - |
52 | | -```sh |
53 | | -google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/reddit-login-captcha |
54 | | -``` |
55 | | - |
56 | | -The important detail is origin: the Reddit reCAPTCHA site key must be executed from `https://www.reddit.com/login/`. A local HTML page or unrelated WebView origin will usually produce an invalid token. |
57 | | - |
58 | | -WebView provider shape: |
59 | | - |
60 | | -```go |
61 | | -CaptchaTokenProvider: func(ctx context.Context, req redditchat.CaptchaRequest) (string, error) { |
62 | | - webview.Navigate(req.PageURL) |
63 | | - token, err := webview.EvaluateJavaScript(ctx, req.EnterpriseExecuteJavaScript()) |
64 | | - if err != nil { |
65 | | - return "", err |
66 | | - } |
67 | | - return token, nil |
68 | | -} |
69 | | -``` |
70 | | - |
71 | | -The WebView is only for reCAPTCHA. `LoginReddit` still submits username, password, and TOTP through the reversed Reddit HTTP endpoints. |
72 | | - |
73 | | -## API Surface |
74 | | - |
75 | | -The package exposes mautrix-backed helpers for: |
76 | | - |
77 | | -- `LoginReddit`, `NewFromRedditLogin`, `CredentialsFromRedditSession`, `RedditSessionFromPlaywrightStorageState` |
78 | | -- `CaptchaTokenProviderFromChromeDebugURL`, `CaptchaTokenFromChromeDebugURL`, `StaticCaptchaTokenProvider` |
79 | | -- `Whoami`, `Capabilities`, `Sync`, `Messages`, `GetEvent`, `JoinedRooms` |
80 | | -- `SearchUsers`, `RoomsWithUser`, `DirectRoomsWithUser` |
81 | | -- `CreateDM`, `CreateOrGetDM`, `CreateGroup`, `Invite`, `AcceptInvite`, `Leave` |
82 | | -- `SendText`, `SendNotice`, `SendMessage` |
83 | | -- `SetTyping`, `MarkRead` |
84 | | -- `MediaConfig`, `UploadMedia`, `SendFile`, `SendImage`, `SendVideo`, `SendAudio`, `SendMedia` |
85 | | -- `SendMXCFile`, `SendMXCImage`, `SendMXCVideo`, `SendMXCAudio`, `SendMXCMedia` |
86 | | -- `DownloadMedia`, `DownloadMediaBytes` |
87 | | - |
88 | | -Raw mautrix access is available through `Client.Mautrix()`. |
89 | | - |
90 | | -## Smoke Command |
91 | | - |
92 | | -Authenticate both saved test sessions and sync: |
93 | | - |
94 | | -```sh |
95 | | -go run ./cmd/redditchat-smoke |
96 | | -``` |
97 | | - |
98 | | -Derive Matrix credentials from Reddit session cookies instead of chat localStorage: |
99 | | - |
100 | | -```sh |
101 | | -go run ./cmd/redditchat-smoke -session-auth |
102 | | -``` |
103 | | - |
104 | | -Use Chrome debug protocol instead of saved storage state: |
105 | | - |
106 | | -```sh |
107 | | -go run ./cmd/redditchat-smoke -cdp-a http://127.0.0.1:9222 -cdp-b http://127.0.0.1:9223 |
108 | | -``` |
109 | | - |
110 | | -Seed real Chrome debug sessions from the saved test states, then authenticate through CDP: |
111 | | - |
112 | | -```sh |
113 | | -go run ./cmd/redditchat-smoke \ |
114 | | - -seed-cdp \ |
115 | | - -cdp-a http://127.0.0.1:9222 \ |
116 | | - -cdp-b http://127.0.0.1:9223 |
117 | | -``` |
118 | | - |
119 | | -Send a text message through the verified DM room: |
120 | | - |
121 | | -```sh |
122 | | -go run ./cmd/redditchat-smoke \ |
123 | | - -room '!7qgWTmcqCdRXuLbvlk3ggseaFXdE5jltGuMrhpSbq6o:reddit.com' \ |
124 | | - -message 'hello from mautrix' |
125 | | -``` |
126 | | - |
127 | | -Get or create a DM, or create a group room, then send the smoke message: |
128 | | - |
129 | | -```sh |
130 | | -go run ./cmd/redditchat-smoke -create-dm |
131 | | -go run ./cmd/redditchat-smoke -create-group |
132 | | -``` |
133 | | - |
134 | | -Send media through the Matrix media API: |
135 | | - |
136 | | -```sh |
137 | | -go run ./cmd/redditchat-smoke \ |
138 | | - -room '!7qgWTmcqCdRXuLbvlk3ggseaFXdE5jltGuMrhpSbq6o:reddit.com' \ |
139 | | - -image /tmp/reddit-chat-smoke.png |
140 | | -``` |
141 | | - |
142 | | -Send an already-hosted Reddit MXC media event: |
143 | | - |
144 | | -```sh |
145 | | -go run ./cmd/redditchat-smoke \ |
146 | | - -room '!7qgWTmcqCdRXuLbvlk3ggseaFXdE5jltGuMrhpSbq6o:reddit.com' \ |
147 | | - -image-url 'mxc://reddit.com/<media-id>' \ |
148 | | - -media-name image.jpg \ |
149 | | - -media-content-type image/jpeg |
150 | | -``` |
151 | | - |
152 | | -Print Reddit Matrix media limits: |
153 | | - |
154 | | -```sh |
155 | | -go run ./cmd/redditchat-smoke -media-config |
156 | | -``` |
157 | | - |
158 | | -## Current Live Evidence |
159 | | - |
160 | | -- Both test accounts authenticate via Reddit's Matrix tokens. |
161 | | -- Reddit login capture reversed the current web endpoints: password login uses `/svc/shreddit/account/login`; 2FA uses `/svc/shreddit/account/login/otp`; chat token refresh uses `POST /svc/shreddit/token`. |
162 | | -- `-session-auth` was verified with a post-login state that had no chat localStorage bootstrap: account A minted fresh Matrix credentials as `@t2_2foi1of47y:reddit.com` device `63491a13e9d7e54e91096051125ff818`, and account B also minted fresh credentials from Reddit session cookies. Both synced successfully. |
163 | | -- Both test accounts now have verified email addresses in Reddit settings. |
164 | | -- `Sync` works for both accounts. |
165 | | -- Reddit's web client uses `preset: "reddit_dm"` for direct chats and `preset: "private_chat"` for group rooms. The wrapper now matches those presets. |
166 | | -- The web client checks `/_matrix/client/v3/rooms?with_user=...&type=direct&include=state,timeline` before creating a DM. `CreateOrGetDM` implements the same reuse flow. |
167 | | -- `CreateDM` worked previously with a minimal mautrix `CreateRoom` request; repeated smoke runs now reuse the existing DM to avoid room-creation quota. |
168 | | -- Account B accepted the invite via `JoinRoomByID`. |
169 | | -- Account A sent a text message via mautrix in the verified DM room and account B observed it via `Sync`. |
170 | | -- Real Chromium CDP sessions were launched on ports 9222 and 9223, seeded from the saved states, and used for auth/sync through `CredentialsFromChromeDebugURL`. |
171 | | -- CDP-backed send/receive worked in room `!7qgWTmcqCdRXuLbvlk3ggseaFXdE5jltGuMrhpSbq6o:reddit.com`; sent event `$bY18t2ZghWQqGKKUy3K2nn52UcwTlatxDjPiNaTi6lA` was observed by account B. |
172 | | -- The latest `CreateOrGetDM` smoke reused that room and sent event `$9Pie9l3KG8pohOoV_vQayz-7nHoVt058QYW5DnjSm1s`, observed by account B. |
173 | | -- `SendMXCImage` with a `mxc://reddit.com/...` URI sent event `$KpRv0U9TGl_0hYbM9Rd9xHSzOXBvzD4Q-95o80OqZMU`, observed by account B. |
174 | | - |
175 | | -New-device note: a message send using a freshly minted session-auth Matrix device reached the existing DM room but Reddit returned `M_FORBIDDEN: User is flagged for spam`. The same accounts can still sync, and earlier established devices can send in the DM. This appears to be another Reddit account/device trust decision rather than a missing Matrix API call. |
176 | | - |
177 | | -Group note: `CreateGroup` is implemented as a non-direct Matrix room creation with invites. Live creation is currently blocked by Reddit rate limits/account-establishment gates on the test accounts: |
178 | | - |
179 | | -- After email verification and chat reload, account A receives `M_LIMIT_EXCEEDED: Limit exceeded for number of invites attempted` with `rate.score_invitation_limit`. |
180 | | -- Earlier runs also hit `rate.score_room_creation_limit` on account A and `rate.score_invitation_limit_ln` when account B was used as the creator. |
181 | | - |
182 | | -Media note: Reddit currently returns `M_FORBIDDEN: Media upload forbidden` for standard Matrix media upload from these sessions, even after both accounts were email verified and the chat app was reloaded. `MediaConfig` reports `m.upload.size: 20971520` and `com.reddit.upload.size: {"image/gif":104857600}`, so the endpoint is present. A headed browser capture of the real Reddit chat UI selected `/tmp/reddit-chat-smoke.png` and the UI itself posted to `https://matrix.redditspace.com/_matrix/media/v3/upload?filename=reddit-chat-smoke.png`, the same endpoint mautrix uses. Reddit returned HTTP 403 and displayed "An error occurred while uploading the image. Please try again." Reddit also rejects HTTPS media URLs and foreign MXC origins in message content, but accepts `mxc://reddit.com/...` media events. A synthetic Reddit-origin MXC URI sends and renders as an image event; a fake media ID will not download actual bytes. |
| 32 | +Local protocol tests use synthetic responses and do not establish end-to-end |
| 33 | +acceptance. |
0 commit comments