Bind Reddit and Matrix session identities

This commit is contained in:
jjbrunton committed 2026-08-08 19:43:35 +01:00
1 parent 34ae532cdd
commit 492efe0f8a
12 files changed
+234 -55

No files matched your search

@@ -13,3 +13,5 @@ This local Chrome extension exports `reddit.com` cookies and the current Reddit
6. Delete the downloaded JSON file once the dashboard confirms that the session is authenticated.
The extension observes only requests to Reddit's Matrix homeservers, verifies the captured token with Matrix `whoami`, reads Reddit cookies, and saves the combined export locally. It does not send the dump to any server; you upload it directly to your RedditBot dashboard.
Before downloading, the extension independently resolves the current Reddit cookie identity and refuses the export unless its `t2` account ID matches Matrix `whoami`. Changing Reddit accounts clears the stored Matrix capture, so Reddit Chat must be reopened after every account switch.
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "RedditBot Session Exporter",
"version": "1.0.0",
"version": "1.1.0",
"description": "Exports Reddit cookies and Reddit Chat credentials for a self-hosted RedditBot dashboard.",
"permissions": ["cookies", "downloads", "storage", "webRequest"],
"host_permissions": [
@@ -1,4 +1,7 @@
import { createSessionExportDownload } from './session-export.js';
import {
assertMatchingSessionIdentity,
createSessionExportDownload,
} from './session-export.js';
const MATRIX_URLS = [
'https://matrix.redditspace.com/*',
@@ -27,6 +30,29 @@ chrome.webRequest.onBeforeSendHeaders.addListener(
['requestHeaders', 'extraHeaders'],
);
chrome.cookies.onChanged.addListener(async ({ cookie }) => {
if (!['reddit_session', 'token_v2'].includes(cookie.name)) return;
await chrome.storage.local.remove('matrixCapture');
await chrome.action.setBadgeText({ text: '' });
await chrome.action.setTitle({
title: 'Reddit account changed. Open Reddit Chat before exporting.',
});
});
async function getRedditIdentity() {
const response = await fetch('https://www.reddit.com/api/me.json', {
credentials: 'include',
cache: 'no-store',
});
if (!response.ok) throw new Error('Reddit cookies did not authenticate. Sign in and try again.');
const payload = await response.json();
const account = payload?.data ?? payload;
if (!account?.name || !account?.id) {
throw new Error('Reddit did not return an authenticated account identity.');
}
return { username: account.name, accountId: account.id };
}
async function showError(message) {
await chrome.action.setBadgeText({ text: 'ERR' });
await chrome.action.setBadgeBackgroundColor({ color: '#dc3545' });
@@ -48,6 +74,14 @@ chrome.action.onClicked.addListener(async () => {
const identity = await whoami.json();
if (!identity?.user_id) throw new Error('Reddit Chat did not return an account identity.');
const redditIdentity = await getRedditIdentity();
try {
assertMatchingSessionIdentity(redditIdentity, identity.user_id);
} catch (error) {
await chrome.storage.local.remove('matrixCapture');
throw error;
}
await chrome.downloads.download({
...createSessionExportDownload(cookies, { ...matrixCapture, userId: identity.user_id }),
saveAs: true,
@@ -1,5 +1,26 @@
const REDDITBOT_SESSION_FILENAME = 'redditbot-session.json';
export function normalizeRedditAccountId(accountId) {
const trimmed = String(accountId ?? '').trim();
const normalized = trimmed.startsWith('t2_') ? trimmed : `t2_${trimmed}`;
if (!/^t2_[a-z0-9]+$/i.test(normalized)) {
throw new Error('Reddit returned an invalid account ID.');
}
return normalized;
}
export function assertMatchingSessionIdentity(redditIdentity, matrixUserId) {
const accountId = normalizeRedditAccountId(redditIdentity?.accountId);
const username = String(redditIdentity?.username ?? '').trim();
if (!username) throw new Error('Reddit did not return an authenticated username.');
const expectedMatrixUserId = `@${accountId}:reddit.com`;
if (matrixUserId !== expectedMatrixUserId) {
throw new Error(
`Reddit is logged in as u/${username} (${accountId}), but Chat is authenticated as ${matrixUserId}. Reload Reddit Chat after switching accounts, then export again.`,
);
}
}
function isRedditDomain(domain) {
const normalised = domain.toLowerCase().replace(/^\./, '');
return normalised === 'reddit.com' || normalised.endsWith('.reddit.com');