326 lines
8.9 KiB
TypeScript
326 lines
8.9 KiB
TypeScript
import { AxiosError } from 'axios'
|
|
import { Entity, entities } from '../Entities'
|
|
import { IdMapping } from '../entity/IdMapping'
|
|
import log from '../helpers/logger'
|
|
import {
|
|
getMapping,
|
|
getMappingByMatrixId,
|
|
getMessageId,
|
|
getRoomId,
|
|
getUserId,
|
|
save,
|
|
} from '../helpers/storage'
|
|
import {
|
|
axios,
|
|
formatUserSessionOptions,
|
|
getUserSessionOptions,
|
|
} from '../helpers/synapse'
|
|
import { acceptInvitation, inviteMember } from './rooms'
|
|
import reactionKeys from '../reactions.json'
|
|
|
|
const applicationServiceToken = process.env.AS_TOKEN || ''
|
|
if (!applicationServiceToken) {
|
|
const message = 'No AS_TOKEN found in .env.'
|
|
log.error(message)
|
|
throw new Error(message)
|
|
}
|
|
|
|
export type RcMessage = {
|
|
_id: string
|
|
t?: string // Event type
|
|
rid: string // The unique id for the room
|
|
msg: string // The content of the message.
|
|
tmid?: string
|
|
ts: {
|
|
$date: string
|
|
}
|
|
mentions?: string[]
|
|
u: {
|
|
_id: string
|
|
username?: string
|
|
name?: string
|
|
}
|
|
// md?: any // The message's content in a markdown format.
|
|
pinned?: boolean
|
|
drid?: string // The direct room id (if belongs to a direct room).
|
|
// attachments?: any[] // An array of attachment objects, available only when the message has at least one attachment.
|
|
reactions?: {
|
|
[key: string]: {
|
|
usernames: string[]
|
|
}
|
|
}
|
|
}
|
|
|
|
export type MatrixMessage = {
|
|
body: string
|
|
msgtype: 'm.text'
|
|
type: 'm.room.message'
|
|
'm.relates_to'?: {
|
|
rel_type: 'm.thread'
|
|
event_id: string
|
|
is_falling_back: true
|
|
'm.in_reply_to': {
|
|
event_id: string
|
|
}
|
|
}
|
|
}
|
|
|
|
export function mapMessage(rcMessage: RcMessage): MatrixMessage {
|
|
return {
|
|
body: rcMessage.msg,
|
|
msgtype: 'm.text',
|
|
type: 'm.room.message',
|
|
}
|
|
}
|
|
|
|
export async function createMapping(
|
|
rcId: string,
|
|
matrixId: string
|
|
): Promise<void> {
|
|
const messageMapping = new IdMapping()
|
|
messageMapping.rcId = rcId
|
|
messageMapping.matrixId = matrixId
|
|
messageMapping.type = entities[Entity.Messages].mappingType
|
|
|
|
await save(messageMapping)
|
|
log.debug('Mapping added:', messageMapping)
|
|
}
|
|
|
|
export async function createMessage(
|
|
matrixMessage: MatrixMessage,
|
|
room_id: string,
|
|
user_id: string,
|
|
ts: number,
|
|
transactionId: string
|
|
): Promise<string> {
|
|
return (
|
|
await axios.put(
|
|
`/_matrix/client/v3/rooms/${room_id}/send/m.room.message/${transactionId}?user_id=${user_id}&ts=${ts}`,
|
|
matrixMessage,
|
|
formatUserSessionOptions(applicationServiceToken)
|
|
)
|
|
).data.event_id
|
|
}
|
|
|
|
export async function handleReactions(
|
|
reactions: object,
|
|
matrixMessageId: string,
|
|
matrixRoomId: string
|
|
): Promise<void> {
|
|
for (const [reaction, value] of Object.entries(reactions)) {
|
|
// Lookup key/emoji
|
|
const reactionKey = (reactionKeys as any)[reaction]
|
|
value.usernames.map(async (rcUsername: string) => {
|
|
// generate transaction id
|
|
const transactionId = Buffer.from(
|
|
[matrixMessageId, reaction, rcUsername].join('\0')
|
|
).toString('base64')
|
|
// lookup user access token
|
|
const userSessionOptions = await getUserSessionOptions(rcUsername)
|
|
log.http(
|
|
`Adding reaction to message ${matrixMessageId} with symbol ${reactionKey} for user ${rcUsername}`
|
|
)
|
|
// put reaction
|
|
await axios.put(
|
|
`/_matrix/client/v3/rooms/${matrixRoomId}/send/m.reaction/${transactionId}`,
|
|
{
|
|
'm.relates_to': {
|
|
rel_type: 'm.annotation',
|
|
event_id: matrixMessageId,
|
|
key: reactionKey,
|
|
},
|
|
},
|
|
userSessionOptions
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
export async function handle(rcMessage: RcMessage): Promise<void> {
|
|
log.info(`Parsing message with ID: ${rcMessage._id}`)
|
|
|
|
const matrixId = await getMessageId(rcMessage._id)
|
|
if (matrixId) {
|
|
log.debug(`Mapping exists: ${rcMessage._id} -> ${matrixId}`)
|
|
return
|
|
}
|
|
|
|
const room_id = await getRoomId(rcMessage.rid)
|
|
if (!room_id) {
|
|
log.warn(
|
|
`Could not find room ${rcMessage.rid} for message ${rcMessage._id}, skipping.`
|
|
)
|
|
return
|
|
}
|
|
|
|
if (rcMessage.t) {
|
|
switch (rcMessage.t) {
|
|
case 'ru': // User removed by
|
|
case 'ul': // User left
|
|
case 'ult': // User left team
|
|
case 'removed-user-from-team': // Removed user from team
|
|
log.info(
|
|
`Message ${rcMessage._id} is of type ${rcMessage.t}, removing member ${rcMessage.msg} from room ${room_id}`
|
|
)
|
|
|
|
const members = (
|
|
await axios.get(
|
|
`/_matrix/client/v3/rooms/${room_id}/joined_members`,
|
|
formatUserSessionOptions(applicationServiceToken)
|
|
)
|
|
).data.joined
|
|
if (!members) {
|
|
const errorMessage = `Could not determine members of room ${room_id}, aborting`
|
|
log.error(errorMessage)
|
|
throw new Error(errorMessage)
|
|
}
|
|
|
|
const matrixUser =
|
|
Object.keys(members).find((key) =>
|
|
key.includes(rcMessage.msg.toLowerCase())
|
|
) || ''
|
|
|
|
const userMapping = await getMappingByMatrixId(matrixUser)
|
|
if (!userMapping?.accessToken) {
|
|
log.warn(
|
|
`Could not get access token for ${rcMessage.msg}, maybe user is not a member, skipping.`
|
|
)
|
|
return
|
|
}
|
|
|
|
log.http(`User ${matrixUser} leaves room ${room_id}`)
|
|
await axios.post(
|
|
`/_matrix/client/v3/rooms/${room_id}/leave`,
|
|
{ reason: `Event type ${rcMessage.t}` },
|
|
formatUserSessionOptions(userMapping.accessToken)
|
|
)
|
|
return
|
|
|
|
case 'uj': // User joined channel
|
|
case 'ujt': // User joined team
|
|
case 'ut': // User joined conversation
|
|
|
|
case 'au': // User added by
|
|
case 'added-user-to-team': // Added user to team
|
|
case 'r': // Room name changed
|
|
case 'rm': // Message removed
|
|
log.warn(
|
|
`Message ${rcMessage._id} is of type ${rcMessage.t}, for which Rocket.Chat does not provide the initial state information, skipping.`
|
|
)
|
|
return
|
|
|
|
case 'user-muted': // User muted by
|
|
default:
|
|
log.warn(
|
|
`Message ${rcMessage._id} is of unhandled type ${rcMessage.t}, skipping.`
|
|
)
|
|
return
|
|
}
|
|
}
|
|
|
|
const user_id = await getUserId(rcMessage.u._id)
|
|
if (!user_id) {
|
|
log.warn(
|
|
`Could not find author ${rcMessage.u.username} for message ${rcMessage._id}, skipping.`
|
|
)
|
|
return
|
|
}
|
|
|
|
const matrixMessage = mapMessage(rcMessage)
|
|
|
|
const ts = new Date(rcMessage.ts.$date).valueOf()
|
|
if (rcMessage.tmid) {
|
|
const event_id = await getMessageId(rcMessage.tmid)
|
|
if (!event_id) {
|
|
log.warn(`Related message ${rcMessage.tmid} missing, skipping.`)
|
|
return
|
|
} else {
|
|
matrixMessage['m.relates_to'] = {
|
|
rel_type: 'm.thread',
|
|
event_id,
|
|
is_falling_back: true,
|
|
'm.in_reply_to': {
|
|
event_id,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
const event_id = await createMessage(
|
|
matrixMessage,
|
|
room_id,
|
|
user_id,
|
|
ts,
|
|
rcMessage._id
|
|
)
|
|
if (rcMessage.reactions) {
|
|
log.info(
|
|
`Parsing reactions for message ${rcMessage._id}`,
|
|
rcMessage.reactions
|
|
)
|
|
await handleReactions(rcMessage, event_id, room_id)
|
|
}
|
|
await createMapping(rcMessage._id, event_id)
|
|
} catch (error) {
|
|
if (
|
|
error instanceof AxiosError &&
|
|
error.response &&
|
|
error.response.data.errcode === 'M_FORBIDDEN' &&
|
|
error.response.data.error === `User ${user_id} not in room ${room_id}`
|
|
) {
|
|
log.info(error.response.data.error + ', adding.')
|
|
|
|
const userMapping = await getMapping(
|
|
rcMessage.u._id,
|
|
entities[Entity.Users].mappingType
|
|
)
|
|
if (!userMapping || !userMapping.matrixId || !userMapping.accessToken) {
|
|
log.warn(`Could not determine joining user, skipping.`, rcMessage)
|
|
return
|
|
}
|
|
|
|
// Get room creator session or use empty axios options
|
|
let userSessionOptions = {}
|
|
const roomCreatorId = (
|
|
await axios.get(`/_synapse/admin/v1/rooms/${room_id}`)
|
|
).data.creator
|
|
if (!roomCreatorId) {
|
|
log.warn(
|
|
`Could not determine room creator for room ${room_id}, using admin credentials.`
|
|
)
|
|
} else {
|
|
const creatorMapping = await getMappingByMatrixId(roomCreatorId)
|
|
if (!creatorMapping?.accessToken) {
|
|
log.warn(`Could not access token for ${roomCreatorId}, skipping.`)
|
|
return
|
|
}
|
|
userSessionOptions = formatUserSessionOptions(
|
|
creatorMapping.accessToken
|
|
)
|
|
}
|
|
|
|
await inviteMember(userMapping.matrixId, room_id, userSessionOptions)
|
|
await acceptInvitation(userMapping, room_id)
|
|
|
|
const event_id = await createMessage(
|
|
matrixMessage,
|
|
room_id,
|
|
user_id,
|
|
ts,
|
|
rcMessage._id
|
|
)
|
|
if (rcMessage.reactions) {
|
|
log.info(
|
|
`Parsing reactions for message ${rcMessage._id}`,
|
|
rcMessage.reactions
|
|
)
|
|
await handleReactions(rcMessage, event_id, room_id)
|
|
}
|
|
await createMapping(rcMessage._id, event_id)
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|