Compare commits

...

10 Commits

Author SHA1 Message Date
Henrik Hüttemann
daeffdfcf7
Fix reactions
- look up missing reaction keys/emojis via library
- skip, if no reaction found
- skip for missing users
- fix async function not completely awaited
2023-10-23 18:12:25 +02:00
Henrik Hüttemann
ca7787a8eb
Add function to find user by RC username 2023-10-23 18:12:25 +02:00
Henrik Hüttemann
ed4c9689ec
Create mapping after successful reaction handling 2023-10-23 18:12:24 +02:00
Henrik Hüttemann
418c79ff55
Exclude admin user from membership check 2023-10-23 18:12:24 +02:00
Henrik Hüttemann
911a358333
Add reactions for messages 2023-10-23 18:12:23 +02:00
Henrik Hüttemann
80ad2133f3
Remove one-off-used library 2023-10-23 18:12:23 +02:00
Henrik Hüttemann
2c73c4f681
Update design decisions regarding reactions 2023-10-23 18:12:22 +02:00
Henrik Hüttemann
cbd37d0a1a
Add reaction mapping 2023-10-23 18:12:21 +02:00
Henrik Hüttemann
f1f7d5905c
Add manual reaction emoji translations 2023-10-23 18:12:21 +02:00
Henrik Hüttemann
0d2b14dcea
WIP: Add script for automatic emoji translation --skip-ci-- 2023-10-23 18:12:20 +02:00
7 changed files with 1157 additions and 7 deletions

View File

@ -49,7 +49,7 @@ app_service_config_files:
- /data/app-service.yaml
```
Now edit `app-service.example.yaml` and save it at `files/app-service.yaml`, changing the tokens.
Now edit `app-service.example.yaml` and save it at `files/app-service.yaml`, changing the tokens manually.
Copy over `.env.example` to `.env` and insert your values.
@ -101,4 +101,8 @@ Then you can restart with an empty but quite equal server, following the instruc
- Getting data from Rocket.Chat via (currently) manual mongodb export
- Room to Channel conversion:
- Read-only attributes of 2 verdigado channels not converted to power levels due to complexity
- Read-only attributes of channels not converted to power levels due to complexity
- Reactions:
- So far only reactions used in our chats have been translated
- Individual logos of *netzbegruenung* and *verdigado* have been replaced by a generic sunflower
- Skin colour tones and genders have been ignored in the manual translation, using the neutral versions

49
package-lock.json generated
View File

@ -12,6 +12,7 @@
"axios": "^1.5.0",
"dotenv": "^16.3.1",
"n-readlines": "^1.0.1",
"node-emoji": "^2.1.0",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.17",
@ -1405,6 +1406,17 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true
},
"node_modules/@sindresorhus/is": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz",
"integrity": "sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
@ -2498,8 +2510,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
}
@ -3204,6 +3214,11 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/emojilib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz",
"integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="
},
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
@ -6317,6 +6332,17 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
},
"node_modules/node-emoji": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.0.tgz",
"integrity": "sha512-tcsBm9C6FmPN5Wo7OjFi9lgMyJjvkAeirmjR/ax8Ttfqy4N8PoFic26uqFTIgayHPNI5FH4ltUvfh9kHzwcK9A==",
"dependencies": {
"@sindresorhus/is": "^3.1.2",
"char-regex": "^1.0.2",
"emojilib": "^2.4.0",
"skin-tone": "^2.0.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -7426,6 +7452,17 @@
"dev": true,
"peer": true
},
"node_modules/skin-tone": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz",
"integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==",
"dependencies": {
"unicode-emoji-modifier-base": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -8292,6 +8329,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/unicode-emoji-modifier-base": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz",
"integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==",
"engines": {
"node": ">=4"
}
},
"node_modules/unique-filename": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",

View File

@ -50,6 +50,7 @@
"axios": "^1.5.0",
"dotenv": "^16.3.1",
"n-readlines": "^1.0.1",
"node-emoji": "^2.1.0",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.17",

View File

@ -75,9 +75,13 @@ async function removeExcessRoomMembers() {
)
// do action for any user in mx, but not in rc
const adminUsername = process.env.ADMIN_USERNAME || ''
await Promise.all(
actualMembers.map(async (actualMember) => {
if (!memberNames.includes(actualMember)) {
if (
!memberNames.includes(actualMember) &&
!actualMember.includes(adminUsername) // exclude admin from removal
) {
log.warn(
`Member ${actualMember} should not be in room ${roomMapping.matrixId}, removing`
)

View File

@ -1,4 +1,5 @@
import { AxiosError } from 'axios'
import * as emoji from 'node-emoji'
import { Entity, entities } from '../Entities'
import { IdMapping } from '../entity/IdMapping'
import log from '../helpers/logger'
@ -8,9 +9,11 @@ import {
getMessageId,
getRoomId,
getUserId,
getUserMappingByName,
save,
} from '../helpers/storage'
import { axios, formatUserSessionOptions } from '../helpers/synapse'
import reactionKeys from '../reactions.json'
import { acceptInvitation, inviteMember } from './rooms'
const applicationServiceToken = process.env.AS_TOKEN || ''
@ -39,7 +42,11 @@ export type RcMessage = {
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?: object // Object containing reaction information associated with the message.
reactions?: {
[key: string]: {
usernames: string[]
}
}
}
export type MatrixMessage = {
@ -93,6 +100,65 @@ export async function createMessage(
).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: string =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(reactionKeys as any)[reaction] || emoji.get(reaction.replaceAll(':', ''))
if (!reactionKey) {
log.warn(
`Could not find an emoji for ${reaction} for message ${matrixMessageId}, skipping`
)
return
}
await Promise.all(
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 userMapping = await getUserMappingByName(rcUsername)
if (!userMapping) {
log.warn(
`Could not find user mapping for name: ${rcUsername}, skipping reaction ${reaction} for message ${matrixMessageId}`
)
return
}
if (!userMapping.accessToken) {
throw new Error(
`User mapping for name ${rcUsername} has no access token`
)
}
const userSessionOptions = formatUserSessionOptions(
userMapping.accessToken
)
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}`)
@ -211,6 +277,13 @@ export async function handle(rcMessage: RcMessage): Promise<void> {
ts,
rcMessage._id
)
if (rcMessage.reactions) {
log.info(
`Parsing reactions for message ${rcMessage._id}`,
rcMessage.reactions
)
await handleReactions(rcMessage.reactions, event_id, room_id)
}
await createMapping(rcMessage._id, event_id)
} catch (error) {
if (
@ -260,6 +333,13 @@ export async function handle(rcMessage: RcMessage): Promise<void> {
ts,
rcMessage._id
)
if (rcMessage.reactions) {
log.info(
`Parsing reactions for message ${rcMessage._id}`,
rcMessage.reactions
)
await handleReactions(rcMessage.reactions, event_id, room_id)
}
await createMapping(rcMessage._id, event_id)
} else {
throw error

View File

@ -1,4 +1,4 @@
import { DataSource } from 'typeorm'
import { DataSource, ILike } from 'typeorm'
import { Entity, entities } from '../Entities'
import { IdMapping } from '../entity/IdMapping'
import { Membership } from '../entity/Membership'
@ -35,6 +35,15 @@ export function getMappingByMatrixId(id: string): Promise<IdMapping | null> {
})
}
export function getUserMappingByName(
username: string
): Promise<IdMapping | null> {
return AppDataSource.manager.findOneBy(IdMapping, {
matrixId: ILike(`@${username.toLowerCase()}:%`),
type: entities[Entity.Users].mappingType,
})
}
export async function save(entity: IdMapping | Membership): Promise<void> {
await AppDataSource.manager.save(entity)
}

1007
src/reactions.json Normal file

File diff suppressed because it is too large Load Diff