Skip to content

Commit fa968f9

Browse files
committed
More matrix setup
1 parent 07cac52 commit fa968f9

6 files changed

Lines changed: 175 additions & 23 deletions

File tree

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,16 @@ echo "Summarize the last three messages" | node scripts/cli.js
237237

238238
### Matrix (Synapse / homeserver) bot
239239

240-
1. On your homeserver, create a dedicated bot user (or use an existing account). Obtain an **access token** (e.g. sign in with [Element](https://element.io) and copy the token from **Help & About → Access Token**, or use `POST /_matrix/client/v3/login` against your Synapse URL).
241-
2. In Config → Channels, enable **Matrix bot**, set **Homeserver URL** to your client API base (e.g. `https://matrix.example.org`), and paste the **access token**. Restart the server.
240+
1. On your homeserver, create a dedicated bot user (or use an existing account).
241+
2. In Config → Channels, enable **Matrix bot** and set **Homeserver URL** to your client API base (e.g. `https://matrix.example.org`). Choose a **sign-in method**:
242+
- **Access token** — paste a token (e.g. from Element **Help & About → Access Token**). Easiest if you already have a token.
243+
- **Username and password (Client-Server login API)** — enter the bot’s Matrix user ID (local part or full `@user:server`) and password. On startup, ShadowAI calls `POST /_matrix/client/v3/login` (same API Element uses) and uses the returned access token for the session. The password is stored in `config.json` when you save; it is not returned in the config API response.
244+
- If **Access token** is selected but the token field is empty, ShadowAI will still try **user ID + password** from config when both are set (fallback).
242245
3. Install the optional dependency: `npm install matrix-bot-sdk`
243-
4. Invite the bot to a room or start a direct message. The bot auto-accepts invites. Each Matrix user gets their own conversation history (unencrypted rooms work out of the box; encrypted rooms require extra crypto setup and are not covered here).
244-
5. **Restrict who can use the bot:** set **Allowed Matrix user IDs** to comma-separated full MXIDs (e.g. `@alice:example.org`). Leave empty to allow any user in rooms where the bot is present.
245-
6. Send `reset` or `!reset` in the room to clear that user’s stored conversation (same idea as Discord `/reset`).
246+
4. Restart the server after saving channel settings.
247+
5. Invite the bot to a room or start a direct message. The bot auto-accepts invites. Each Matrix user gets their own conversation history (unencrypted rooms work out of the box; encrypted rooms require extra crypto setup and are not covered here).
248+
6. **Restrict who can use the bot:** set **Allowed Matrix user IDs** to comma-separated full MXIDs (e.g. `@alice:example.org`). Leave empty to allow any user in rooms where the bot is present.
249+
7. Send `reset` or `!reset` in the room to clear that user’s stored conversation (same idea as Discord `/reset`).
246250

247251
Channel conversations (CLI, Telegram, Discord, Matrix) are stored per synthetic user id (e.g. `channel_cli`, `telegram_<userId>`, `discord_<userId>`, `matrix_<localpart_homeserver>`) and appear in the web UI chat list so you can inspect or continue them from the browser. CLEAR in the web UI, or `/reset` in Discord / `reset` in Matrix, will clear that channel conversation.
248252

config.default.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"server":{"host":"0.0.0.0","port":9090},"auth":{"username":"admin","passwordHash":"admin"},"ollama":{"mainUrl":"http://localhost:11434","mainModel":"llama3.2","temperature":0.7,"num_predict":2048,"agents":[{"id":"coding","name":"Coding Agent","url":"http://localhost:11434","model":"codellama","enabled":true}]},"heartbeat":[],"skills":{"enabledIds":[]},"searxng":{"url":"","enabled":false},"email":{"host":"","port":25,"secure":false,"auth":{"user":"","pass":""},"from":"","defaultTo":"","enabled":false},"channels":{"apiKey":"","telegram":{"enabled":false,"botToken":""},"discord":{"enabled":false,"botToken":"","allowedUserIds":[]},"matrix":{"enabled":false,"homeserverUrl":"","accessToken":"","allowedUserIds":[]}}}
1+
{"server":{"host":"0.0.0.0","port":9090},"auth":{"username":"admin","passwordHash":"admin"},"ollama":{"mainUrl":"http://localhost:11434","mainModel":"llama3.2","temperature":0.7,"num_predict":2048,"agents":[{"id":"coding","name":"Coding Agent","url":"http://localhost:11434","model":"codellama","enabled":true}]},"heartbeat":[],"skills":{"enabledIds":[]},"searxng":{"url":"","enabled":false},"email":{"host":"","port":25,"secure":false,"auth":{"user":"","pass":""},"from":"","defaultTo":"","enabled":false},"channels":{"apiKey":"","telegram":{"enabled":false,"botToken":""},"discord":{"enabled":false,"botToken":"","allowedUserIds":[]},"matrix":{"enabled":false,"authMode":"token","homeserverUrl":"","userId":"","accessToken":"","password":"","allowedUserIds":[]}}}

lib/matrixBot.js

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,91 @@ function matrixUsernameForSender(sender) {
1616
return ('matrix_' + local).slice(0, 64);
1717
}
1818

19+
/**
20+
* Obtain an access token via Matrix Client-Server API (Synapse and spec-compliant servers).
21+
* @param {string} homeserverUrl - e.g. https://matrix.example.org
22+
* @param {string} userId - local part (alice) or full MXID (@alice:example.org)
23+
* @param {string} password
24+
*/
25+
async function matrixLoginWithPassword(homeserverUrl, userId, password) {
26+
const base = homeserverUrl.replace(/\/$/, '');
27+
const url = `${base}/_matrix/client/v3/login`;
28+
const body = {
29+
type: 'm.login.password',
30+
identifier: {
31+
type: 'm.id.user',
32+
user: userId.trim()
33+
},
34+
password: String(password),
35+
initial_device_display_name: 'ShadowAI'
36+
};
37+
const res = await fetch(url, {
38+
method: 'POST',
39+
headers: { 'Content-Type': 'application/json' },
40+
body: JSON.stringify(body)
41+
});
42+
const data = await res.json().catch(() => ({}));
43+
if (!res.ok) {
44+
const msg = data.error || data.errcode || res.statusText || 'login failed';
45+
throw new Error(msg);
46+
}
47+
if (!data.access_token) {
48+
throw new Error('Matrix login: no access_token in response');
49+
}
50+
return data.access_token;
51+
}
52+
1953
/**
2054
* Matrix (Synapse / any Matrix homeserver) bot using matrix-bot-sdk.
21-
* Configure in CONFIG → Channels: homeserver URL, bot access token, optional allowlist.
22-
* Create a bot user on the server, log in once to obtain an access token (e.g. Element or curl).
55+
* Configure in CONFIG → Channels: homeserver URL, and either an access token or username+password (login API).
2356
* Invite the bot to a room or DM; optional dependency: npm install matrix-bot-sdk
2457
*/
25-
function startMatrixBot() {
58+
async function startMatrixBotAsync() {
2659
const channels = getConfig().channels || {};
2760
const mx = channels.matrix || {};
2861
if (!mx.enabled) return;
2962
const homeserverUrl = (mx.homeserverUrl || '').trim();
30-
const accessToken = (mx.accessToken || '').trim();
31-
if (!homeserverUrl || !accessToken) {
32-
logger.warn('Matrix bot: enabled but homeserverUrl or accessToken is missing');
63+
if (!homeserverUrl) {
64+
logger.warn('Matrix bot: enabled but homeserverUrl is missing');
65+
return;
66+
}
67+
68+
const authMode = String(mx.authMode || 'token').toLowerCase() === 'password' ? 'password' : 'token';
69+
let accessToken = (mx.accessToken || '').trim();
70+
71+
if (authMode === 'password') {
72+
const userId = (mx.userId || '').trim();
73+
const password = (mx.password || '').trim();
74+
if (!userId || !password) {
75+
logger.warn('Matrix bot: auth mode is password but userId or password is missing');
76+
return;
77+
}
78+
try {
79+
accessToken = await matrixLoginWithPassword(homeserverUrl, userId, password);
80+
logger.info('Matrix bot: signed in via Client-Server login API');
81+
} catch (e) {
82+
logger.error('Matrix bot: login API failed:', e.message);
83+
return;
84+
}
85+
} else if (!accessToken) {
86+
const userId = (mx.userId || '').trim();
87+
const password = (mx.password || '').trim();
88+
if (userId && password) {
89+
try {
90+
accessToken = await matrixLoginWithPassword(homeserverUrl, userId, password);
91+
logger.info('Matrix bot: no access token in config; signed in with stored userId and password');
92+
} catch (e) {
93+
logger.error('Matrix bot: login API failed:', e.message);
94+
return;
95+
}
96+
} else {
97+
logger.warn('Matrix bot: set an access token, or userId + password, or switch auth to password mode');
98+
return;
99+
}
100+
}
101+
102+
if (!accessToken) {
103+
logger.warn('Matrix bot: could not obtain access token');
33104
return;
34105
}
35106

@@ -129,6 +200,12 @@ function startMatrixBot() {
129200
});
130201
}
131202

203+
function startMatrixBot() {
204+
startMatrixBotAsync().catch((e) => {
205+
logger.error('Matrix bot:', e.message);
206+
});
207+
}
208+
132209
function stopMatrixBot() {
133210
if (clientInstance && typeof clientInstance.stop === 'function') {
134211
clientInstance.stop();

public/config.html

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,29 @@ <h2>Channels (CLI, Telegram, Discord, Matrix)</h2>
214214
<span class="section-desc">Client API base (same host you use in Element; no trailing path).</span>
215215
</div>
216216
<div class="form-group">
217+
<label>Matrix sign-in method</label>
218+
<select id="matrixAuthMode" aria-label="Matrix sign-in method">
219+
<option value="token">Access token</option>
220+
<option value="password">Username and password (Client-Server login API)</option>
221+
</select>
222+
<span class="section-desc">Use a token you paste, or store the bot’s Matrix ID and password and let the server call <code>POST /_matrix/client/v3/login</code> on startup.</span>
223+
</div>
224+
<div class="form-group" id="matrixAuthTokenGroup">
217225
<label>Matrix access token</label>
218226
<input type="password" id="matrixAccessToken" placeholder="Bot user access token" autocomplete="off" />
219-
<span class="section-desc">Create a bot user on the server, then obtain a token (e.g. login via Element or <code>/_matrix/client/v3/login</code>).</span>
227+
<span class="section-desc">From Element (Help → Access Token) or any client; used when sign-in method is Access token.</span>
228+
</div>
229+
<div id="matrixAuthPasswordWrap" hidden>
230+
<div class="form-group">
231+
<label>Matrix user ID</label>
232+
<input type="text" id="matrixUserId" placeholder="localpart or @user:server" autocomplete="off" />
233+
<span class="section-desc">Bot account: local username or full MXID for this homeserver.</span>
234+
</div>
235+
<div class="form-group">
236+
<label>Matrix password</label>
237+
<input type="password" id="matrixPassword" placeholder="Leave blank after saving to keep unchanged" autocomplete="new-password" />
238+
<span class="section-desc">Stored in <code>config.json</code> on save. Shown empty when you reload; type again only to change it.</span>
239+
</div>
220240
</div>
221241
<div class="form-group">
222242
<label>Allowed Matrix user IDs</label>

public/config.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@
3838
statusEl.style.color = isError ? 'var(--red)' : 'var(--text-dim)';
3939
}
4040

41+
function toggleMatrixAuthFields() {
42+
const modeEl = document.getElementById('matrixAuthMode');
43+
if (!modeEl) return;
44+
const mode = modeEl.value || 'token';
45+
const tokenGr = document.getElementById('matrixAuthTokenGroup');
46+
const pwdWrap = document.getElementById('matrixAuthPasswordWrap');
47+
if (tokenGr) tokenGr.hidden = mode === 'password';
48+
if (pwdWrap) pwdWrap.hidden = mode !== 'password';
49+
}
50+
4151
function refreshAvatarPreview() {
4252
if (!avatarImg) return;
4353
const url = '/static/ai-avatar?ts=' + Date.now();
@@ -84,8 +94,14 @@
8494
document.getElementById('discordAllowedUserIds').value = Array.isArray(ch.discord?.allowedUserIds) ? ch.discord.allowedUserIds.join(', ') : (ch.discord?.allowedUserIds ?? '');
8595
document.getElementById('matrixEnabled').checked = ch.matrix?.enabled === true;
8696
document.getElementById('matrixHomeserverUrl').value = ch.matrix?.homeserverUrl ?? '';
97+
const matrixModeEl = document.getElementById('matrixAuthMode');
98+
if (matrixModeEl) matrixModeEl.value = ch.matrix?.authMode === 'password' ? 'password' : 'token';
99+
document.getElementById('matrixUserId').value = ch.matrix?.userId ?? '';
87100
document.getElementById('matrixAccessToken').value = ch.matrix?.accessToken ?? '';
101+
const mp = document.getElementById('matrixPassword');
102+
if (mp) mp.value = '';
88103
document.getElementById('matrixAllowedUserIds').value = Array.isArray(ch.matrix?.allowedUserIds) ? ch.matrix.allowedUserIds.join(', ') : (ch.matrix?.allowedUserIds ?? '');
104+
toggleMatrixAuthFields();
89105
const ui = c.ui || {};
90106
document.getElementById('appName').value = ui.appName ?? 'SHADOW_AI';
91107
document.getElementById('showToolCalls').checked = ui.showToolCalls !== false;
@@ -139,12 +155,22 @@
139155
botToken: document.getElementById('discordBotToken').value.trim(),
140156
allowedUserIds: document.getElementById('discordAllowedUserIds').value.split(',').map(s => s.trim()).filter(Boolean)
141157
},
142-
matrix: {
143-
enabled: document.getElementById('matrixEnabled').checked,
144-
homeserverUrl: document.getElementById('matrixHomeserverUrl').value.trim(),
145-
accessToken: document.getElementById('matrixAccessToken').value.trim(),
146-
allowedUserIds: document.getElementById('matrixAllowedUserIds').value.split(',').map(s => s.trim()).filter(Boolean)
147-
}
158+
matrix: (() => {
159+
const authMode = (document.getElementById('matrixAuthMode') && document.getElementById('matrixAuthMode').value) || 'token';
160+
const row = {
161+
enabled: document.getElementById('matrixEnabled').checked,
162+
authMode,
163+
homeserverUrl: document.getElementById('matrixHomeserverUrl').value.trim(),
164+
userId: document.getElementById('matrixUserId').value.trim(),
165+
accessToken: document.getElementById('matrixAccessToken').value.trim(),
166+
allowedUserIds: document.getElementById('matrixAllowedUserIds').value.split(',').map(s => s.trim()).filter(Boolean)
167+
};
168+
const pwdEl = document.getElementById('matrixPassword');
169+
const pwdVal = pwdEl ? pwdEl.value : '';
170+
if (pwdVal) row.password = pwdVal;
171+
else if (authMode === 'token') row.password = '';
172+
return row;
173+
})()
148174
};
149175
}
150176

@@ -379,8 +405,14 @@
379405
document.getElementById('discordAllowedUserIds').value = Array.isArray(ch.discord?.allowedUserIds) ? ch.discord.allowedUserIds.join(', ') : (ch.discord?.allowedUserIds ?? '');
380406
document.getElementById('matrixEnabled').checked = ch.matrix?.enabled === true;
381407
document.getElementById('matrixHomeserverUrl').value = ch.matrix?.homeserverUrl ?? '';
408+
const matrixModeEl2 = document.getElementById('matrixAuthMode');
409+
if (matrixModeEl2) matrixModeEl2.value = ch.matrix?.authMode === 'password' ? 'password' : 'token';
410+
document.getElementById('matrixUserId').value = ch.matrix?.userId ?? '';
382411
document.getElementById('matrixAccessToken').value = ch.matrix?.accessToken ?? '';
412+
const mp2 = document.getElementById('matrixPassword');
413+
if (mp2) mp2.value = '';
383414
document.getElementById('matrixAllowedUserIds').value = Array.isArray(ch.matrix?.allowedUserIds) ? ch.matrix.allowedUserIds.join(', ') : (ch.matrix?.allowedUserIds ?? '');
415+
toggleMatrixAuthFields();
384416
const ui = c.ui || {};
385417
document.getElementById('appName').value = ui.appName ?? 'SHADOW_AI';
386418
document.getElementById('showToolCalls').checked = ui.showToolCalls !== false;
@@ -392,5 +424,8 @@
392424
}
393425
});
394426

427+
const matrixAuthModeEl = document.getElementById('matrixAuthMode');
428+
if (matrixAuthModeEl) matrixAuthModeEl.addEventListener('change', toggleMatrixAuthFields);
429+
395430
loadConfig();
396431
})();

0 commit comments

Comments
 (0)