import { createHmac } from 'node:crypto' import { Entity, entities } from '../Entities' import adminAccessToken from '../config/synapse_access_token.json' import { IdMapping } from '../entity/IdMapping' import log from '../helpers/logger' import { createMembership, getUserId, save } from '../helpers/storage' import { axios, getUserDomain } from '../helpers/synapse' export type RcUser = { _id: string username: string name: string roles: string[] __rooms: string[] } export type MatrixUser = { user_id: string username: string displayname: string password: string admin: boolean nonce?: string mac?: string access_token?: string } export type AccessToken = { access_token: string device_id: string home_server: string user_id: string } export type UserInfo = { admin: boolean displayname: string name: string } export function mapUser(rcUser: RcUser): MatrixUser { return { user_id: '', username: rcUser.username, displayname: rcUser.name, password: '', admin: rcUser.roles.includes('admin'), } } const registrationSharedSecret = process.env.REGISTRATION_SHARED_SECRET || '' if (!registrationSharedSecret) { const message = 'No REGISTRATION_SHARED_SECRET found in .env.' log.error(message) throw new Error(message) } const adminUsername = process.env.ADMIN_USERNAME || '' if (!adminUsername) { const message = 'No ADMIN_USERNAME found in .env.' log.error(message) throw new Error(message) } export function generateHmac(user: MatrixUser): string { const hmac = createHmac('sha1', registrationSharedSecret) hmac.write( `${user.nonce}\0${user.username}\0${user.password}\0${ user.admin ? 'admin' : 'notadmin' }` ) hmac.end() return hmac.read().toString('hex') } async function getUserRegistrationNonce(): Promise { return (await axios.get('/_synapse/admin/v1/register')).data.nonce } async function registerUser(user: MatrixUser): Promise { return (await axios.post('/_synapse/admin/v1/register', user)).data } async function getUserData(user: MatrixUser): Promise { return (await axios.get('/_synapse/admin/v2/users/@' + user.username + ":" + getUserDomain())).data } async function getUserLogin(user: MatrixUser): Promise { return (await axios.get('/_synapse/admin/v1/users/@' + user.username + ":" + getUserDomain() + "/login")).data } async function parseUserMemberships(rcUser: RcUser): Promise { await Promise.all( rcUser.__rooms.map(async (rcRoomId: string) => { await createMembership(rcRoomId, rcUser._id) log.debug(`${rcUser.username} membership for ${rcRoomId} created`) }) ) } export function userIsExcluded(rcUser: RcUser): boolean { const reasons: string[] = [] const excludedUsers = (process.env.EXCLUDED_USERS || '').split(',') if (rcUser.roles.includes('app')) reasons.push('has role "app"') if (rcUser.roles.includes('bot')) reasons.push('has role "bot"') if (excludedUsers.includes(rcUser._id)) reasons.push(`id "${rcUser._id}" is on exclusion list`) if (excludedUsers.includes(rcUser.username)) reasons.push(`username "${rcUser.username}" is on exclusion list`) if (reasons.length > 0) { log.warn(`User ${rcUser.name} is excluded: ${reasons.join(', ')}`) return true } return false } export async function createMapping( rcId: string, matrixUser: MatrixUser ): Promise { const mapping = new IdMapping() mapping.rcId = rcId mapping.matrixId = matrixUser.user_id mapping.type = entities[Entity.Users].mappingType mapping.accessToken = matrixUser.access_token await save(mapping) log.debug('Mapping added:', mapping) } export async function createUser(rcUser: RcUser): Promise { const user = mapUser(rcUser) try { const userData = await getUserData(user) user.user_id = userData.name user.displayname = userData.displayname user.admin = user.admin || userData.admin const accessToken = await getUserLogin(user) user.access_token = accessToken.access_token log.info(`User ${rcUser.username} exists:`, user) } catch (error) { log.error(`Error:`, error) const nonce = await getUserRegistrationNonce() const mac = generateHmac({ ...user, nonce }) const accessToken = await registerUser({ ...user, nonce, mac }) user.user_id = accessToken.user_id user.access_token = accessToken.access_token log.info(`User ${rcUser.username} created:`, user) } await parseUserMemberships(rcUser) return user } export async function handle(rcUser: RcUser): Promise { log.info(`Parsing user: ${rcUser.name}: ${rcUser._id}`) const matrixId = await getUserId(rcUser._id) if (matrixId) { log.debug(`Mapping exists: ${rcUser._id} -> ${matrixId}`) } else { if (rcUser.username === adminUsername) { log.info( `User ${rcUser.username} is defined as admin in ENV, mapping as such` ) await createMapping(rcUser._id, adminAccessToken as unknown as MatrixUser) } else if (!userIsExcluded(rcUser)) { const matrixUser = await createUser(rcUser) await createMapping(rcUser._id, matrixUser) } } }