feat: Matrix Space membership sync API (invite/kick members from org space + child rooms)
This commit is contained in:
186
src/routes/api/matrix-space/members/+server.ts
Normal file
186
src/routes/api/matrix-space/members/+server.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const db = (supabase: any) => supabase;
|
||||
|
||||
/**
|
||||
* POST: Invite a user to the org's Matrix Space (and its child rooms).
|
||||
*
|
||||
* Body: { org_id, matrix_user_id, homeserver_url, access_token }
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const session = await locals.safeGetSession();
|
||||
if (!session.user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { org_id, matrix_user_id, homeserver_url, access_token } = body;
|
||||
|
||||
if (!org_id || !matrix_user_id || !homeserver_url || !access_token) {
|
||||
return json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get org's Matrix Space ID
|
||||
const { data: org } = await db(locals.supabase)
|
||||
.from('organizations')
|
||||
.select('matrix_space_id')
|
||||
.eq('id', org_id)
|
||||
.single();
|
||||
|
||||
if (!org?.matrix_space_id) {
|
||||
return json({ error: 'Organization has no Matrix Space' }, { status: 404 });
|
||||
}
|
||||
|
||||
const spaceId = org.matrix_space_id;
|
||||
const errors: string[] = [];
|
||||
|
||||
// Invite to the Space itself
|
||||
const inviteRes = await matrixInvite(homeserver_url, access_token, spaceId, matrix_user_id);
|
||||
if (!inviteRes.ok) {
|
||||
const err = await inviteRes.json().catch(() => ({}));
|
||||
// M_FORBIDDEN means already joined, which is fine
|
||||
if (err.errcode !== 'M_FORBIDDEN') {
|
||||
errors.push(`Space invite: ${err.error || 'failed'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Also invite to all child rooms of the space
|
||||
try {
|
||||
const stateRes = await fetch(
|
||||
`${homeserver_url}/_matrix/client/v3/rooms/${encodeURIComponent(spaceId)}/state`,
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${access_token}` },
|
||||
}
|
||||
);
|
||||
|
||||
if (stateRes.ok) {
|
||||
const stateEvents = await stateRes.json();
|
||||
const childRoomIds = stateEvents
|
||||
.filter((e: any) => e.type === 'm.space.child' && e.content?.via)
|
||||
.map((e: any) => e.state_key);
|
||||
|
||||
for (const childRoomId of childRoomIds) {
|
||||
const childInvite = await matrixInvite(homeserver_url, access_token, childRoomId, matrix_user_id);
|
||||
if (!childInvite.ok) {
|
||||
const err = await childInvite.json().catch(() => ({}));
|
||||
if (err.errcode !== 'M_FORBIDDEN') {
|
||||
errors.push(`Room ${childRoomId}: ${err.error || 'failed'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push('Failed to fetch space children');
|
||||
}
|
||||
|
||||
return json({
|
||||
success: errors.length === 0,
|
||||
invited: matrix_user_id,
|
||||
spaceId,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE: Kick a user from the org's Matrix Space (and its child rooms).
|
||||
*
|
||||
* Query: ?org_id=...&matrix_user_id=...&homeserver_url=...&access_token=...
|
||||
*/
|
||||
export const DELETE: RequestHandler = async ({ url, locals }) => {
|
||||
const session = await locals.safeGetSession();
|
||||
if (!session.user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const org_id = url.searchParams.get('org_id');
|
||||
const matrix_user_id = url.searchParams.get('matrix_user_id');
|
||||
const homeserver_url = url.searchParams.get('homeserver_url');
|
||||
const access_token = url.searchParams.get('access_token');
|
||||
|
||||
if (!org_id || !matrix_user_id || !homeserver_url || !access_token) {
|
||||
return json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { data: org } = await db(locals.supabase)
|
||||
.from('organizations')
|
||||
.select('matrix_space_id')
|
||||
.eq('id', org_id)
|
||||
.single();
|
||||
|
||||
if (!org?.matrix_space_id) {
|
||||
return json({ error: 'Organization has no Matrix Space' }, { status: 404 });
|
||||
}
|
||||
|
||||
const spaceId = org.matrix_space_id;
|
||||
const errors: string[] = [];
|
||||
|
||||
// Kick from child rooms first, then from the space
|
||||
try {
|
||||
const stateRes = await fetch(
|
||||
`${homeserver_url}/_matrix/client/v3/rooms/${encodeURIComponent(spaceId)}/state`,
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${access_token}` },
|
||||
}
|
||||
);
|
||||
|
||||
if (stateRes.ok) {
|
||||
const stateEvents = await stateRes.json();
|
||||
const childRoomIds = stateEvents
|
||||
.filter((e: any) => e.type === 'm.space.child' && e.content?.via)
|
||||
.map((e: any) => e.state_key);
|
||||
|
||||
for (const childRoomId of childRoomIds) {
|
||||
const kickRes = await matrixKick(homeserver_url, access_token, childRoomId, matrix_user_id);
|
||||
if (!kickRes.ok) {
|
||||
const err = await kickRes.json().catch(() => ({}));
|
||||
if (err.errcode !== 'M_FORBIDDEN') {
|
||||
errors.push(`Room ${childRoomId}: ${err.error || 'failed'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push('Failed to fetch space children');
|
||||
}
|
||||
|
||||
// Kick from the space itself
|
||||
const kickRes = await matrixKick(homeserver_url, access_token, spaceId, matrix_user_id);
|
||||
if (!kickRes.ok) {
|
||||
const err = await kickRes.json().catch(() => ({}));
|
||||
if (err.errcode !== 'M_FORBIDDEN') {
|
||||
errors.push(`Space kick: ${err.error || 'failed'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: errors.length === 0,
|
||||
kicked: matrix_user_id,
|
||||
spaceId,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper: invite a user to a room
|
||||
async function matrixInvite(homeserver: string, token: string, roomId: string, userId: string) {
|
||||
return fetch(`${homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId }),
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: kick a user from a room
|
||||
async function matrixKick(homeserver: string, token: string, roomId: string, userId: string) {
|
||||
return fetch(`${homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/kick`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId, reason: 'Removed from organization' }),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user