Fix Matrix->Telegram formatting and add mention bridging
This commit is contained in:
+6
-7
@@ -65,12 +65,6 @@
|
|||||||
"warn",
|
"warn",
|
||||||
120
|
120
|
||||||
],
|
],
|
||||||
"no-underscore-dangle": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"allowAfterThis": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": [
|
"no-unused-vars": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
@@ -143,6 +137,10 @@
|
|||||||
"allowEmptyCatch": true
|
"allowEmptyCatch": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"no-cond-assign": [
|
||||||
|
"error",
|
||||||
|
"except-parens"
|
||||||
|
],
|
||||||
"function-paren-newline": "off",
|
"function-paren-newline": "off",
|
||||||
"no-labels": "off",
|
"no-labels": "off",
|
||||||
"no-control-regex": "off",
|
"no-control-regex": "off",
|
||||||
@@ -170,6 +168,7 @@
|
|||||||
"no-template-curly-in-string": "off",
|
"no-template-curly-in-string": "off",
|
||||||
"no-await-in-loop": "off",
|
"no-await-in-loop": "off",
|
||||||
"no-restricted-globals": "off",
|
"no-restricted-globals": "off",
|
||||||
"no-fallthrough": "off"
|
"no-fallthrough": "off",
|
||||||
|
"no-underscore-dangle": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ If you don't know the MXID of the puppet, you can search for users using the `se
|
|||||||
* Matrix → Telegram
|
* Matrix → Telegram
|
||||||
* [x] Plaintext messages
|
* [x] Plaintext messages
|
||||||
* [x] Formatted messages
|
* [x] Formatted messages
|
||||||
* [ ] Non-plaintext mentions
|
* [x] Mentions
|
||||||
* [x] Locations
|
* [x] Locations
|
||||||
* [ ] Images
|
* [ ] Images
|
||||||
* [ ] Files
|
* [ ] Files
|
||||||
@@ -55,7 +55,7 @@ If you don't know the MXID of the puppet, you can search for users using the `se
|
|||||||
* Telegram → Matrix
|
* Telegram → Matrix
|
||||||
* [x] Plaintext messages
|
* [x] Plaintext messages
|
||||||
* [x] Formatted messages
|
* [x] Formatted messages
|
||||||
* [ ] Non-plaintext mentions
|
* [x] Mentions
|
||||||
* [x] Images
|
* [x] Images
|
||||||
* [x] Locations
|
* [x] Locations
|
||||||
* [ ] Stickers (somewhat works through document upload, no preview though)
|
* [ ] Stickers (somewhat works through document upload, no preview though)
|
||||||
|
|||||||
+5
-1
@@ -159,6 +159,10 @@ class MautrixTelegram {
|
|||||||
return this.config.bridge.username_template.replace("${ID}", id)
|
return this.config.bridge.username_template.replace("${ID}", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMXIDForTelegramUser(id) {
|
||||||
|
return `@${this.getUsernameForTelegramUser(id)}:${this.config.homeserver.domain}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the matrix.to link for the Matrix puppet of the Telegram user with the given ID.
|
* Get the matrix.to link for the Matrix puppet of the Telegram user with the given ID.
|
||||||
*
|
*
|
||||||
@@ -166,7 +170,7 @@ class MautrixTelegram {
|
|||||||
* @returns {string} A matrix.to link that points to the Matrix puppet of the given user.
|
* @returns {string} A matrix.to link that points to the Matrix puppet of the given user.
|
||||||
*/
|
*/
|
||||||
getMatrixToLinkForTelegramUser(id) {
|
getMatrixToLinkForTelegramUser(id) {
|
||||||
return `https://matrix.to/#/@${this.getUsernameForTelegramUser(id)}:${this.config.homeserver.domain}`
|
return `https://matrix.to/#/${this.getMXIDForTelegramUser(id)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+182
-88
@@ -43,8 +43,9 @@ function addTag(tags, entity, tag, attrs, priority = 0) {
|
|||||||
*
|
*
|
||||||
* @param {string} message The plaintext message.
|
* @param {string} message The plaintext message.
|
||||||
* @param {Array} entities The Telegram formatting entities.
|
* @param {Array} entities The Telegram formatting entities.
|
||||||
|
* @param {MautrixTelegram} app The app main class instance to use when reformatting mentions.
|
||||||
*/
|
*/
|
||||||
function telegramToMatrix(message, entities) {
|
function telegramToMatrix(message, entities, app) {
|
||||||
const tags = []
|
const tags = []
|
||||||
// Decreasing priority counter used to ensure that formattings right next to eachother don't flip like this:
|
// Decreasing priority counter used to ensure that formattings right next to eachother don't flip like this:
|
||||||
// *bold*_italic_ --> <strong>bold<em></strong>italic</em>
|
// *bold*_italic_ --> <strong>bold<em></strong>italic</em>
|
||||||
@@ -52,7 +53,7 @@ function telegramToMatrix(message, entities) {
|
|||||||
|
|
||||||
// Convert Telegram formatting entities into a weird custom indexed HTML tag format thingy.
|
// Convert Telegram formatting entities into a weird custom indexed HTML tag format thingy.
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
let url, tag
|
let url, tag, mxid
|
||||||
switch (entity._) {
|
switch (entity._) {
|
||||||
case "messageEntityBold":
|
case "messageEntityBold":
|
||||||
tag = tag || "strong"
|
tag = tag || "strong"
|
||||||
@@ -72,9 +73,44 @@ function telegramToMatrix(message, entities) {
|
|||||||
// TODO bridge bot commands differently?
|
// TODO bridge bot commands differently?
|
||||||
addTag(tags, entity, "font", "color=\"blue\"", --pc)
|
addTag(tags, entity, "font", "color=\"blue\"", --pc)
|
||||||
break
|
break
|
||||||
|
case "messageEntityMentionName":
|
||||||
|
let user = app.matrixUsersByTelegramID.get(entity.user_id)
|
||||||
|
if (!user) {
|
||||||
|
// TODO this loop step should be made useless
|
||||||
|
for (const userByMXID of app.matrixUsersByID.values()) {
|
||||||
|
if (userByMXID.telegramUserID === entity.user_id) {
|
||||||
|
user = userByMXID
|
||||||
|
app.matrixUsersByTelegramID.set(userByMXID.telegramUserID, userByMXID)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mxid = user ?
|
||||||
|
user.userID :
|
||||||
|
app.getMXIDForTelegramUser(entity.user_id)
|
||||||
case "messageEntityMention":
|
case "messageEntityMention":
|
||||||
// TODO bridge mentions properly?
|
if (!mxid) {
|
||||||
addTag(tags, entity, "font", "color=\"red\"", --pc)
|
const username = message.substr(entity.offset + 1, entity.length - 1)
|
||||||
|
for (const userByMXID of app.matrixUsersByID.values()) {
|
||||||
|
if (userByMXID._telegramPuppet && userByMXID._telegramPuppet.data.username === username) {
|
||||||
|
mxid = userByMXID.userID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mxid) {
|
||||||
|
for (const userByID of app.telegramUsersByID.values()) {
|
||||||
|
if (userByID.username === username) {
|
||||||
|
mxid = userByID.mxid
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mxid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addTag(tags, entity, "a", `href="https://matrix.to/#/${mxid}"`)
|
||||||
break
|
break
|
||||||
case "messageEntityEmail":
|
case "messageEntityEmail":
|
||||||
url = url || `mailto:${message.substr(entity.offset, entity.length)}`
|
url = url || `mailto:${message.substr(entity.offset, entity.length)}`
|
||||||
@@ -101,22 +137,125 @@ function telegramToMatrix(message, entities) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Formatting that is converted back to text
|
// Formatting that is converted back to text
|
||||||
const paragraphs = /<p>(.*?)<\/p>/g
|
const linebreaks = /<br(.*?)>(\n)?/g
|
||||||
const headers = /<h([0-6])>(.*?)<\/h[0-6]>/g
|
const paragraphs = /<p>([^]*?)<\/p>/g
|
||||||
const unorderedLists = /<ul>((.|\n)*?)<\/ul>/g
|
const headers = /<h([0-6])>([^]*?)<\/h[0-6]>/g
|
||||||
const orderedLists = /<ol>((.|\n)*?)<\/ol>/g
|
const unorderedLists = /<ul>([^]*?)<\/ul>/g
|
||||||
const listEntries = /<li>(.*?)<\/li>/g
|
const orderedLists = /<ol>([^]*?)<\/ol>/g
|
||||||
|
const listEntries = /<li>([^]*?)<\/li>/g
|
||||||
|
|
||||||
// Formatting that is converted to Telegram entity formatting
|
// Formatting that is converted to Telegram entity formatting
|
||||||
const boldText = /<strong>((.|\n)*?)<\/strong>/g
|
const boldText = /<(strong)>()([^]*?)<\/strong>/g
|
||||||
const italicText = /<em>((.|\n)*?)<\/em>/g
|
const italicText = /<(em)>()([^]*?)<\/em>/g
|
||||||
const codeblocks = /<pre><code>((.|\n)*?)<\/code><\/pre>/g
|
const codeblocks = /<(pre><code)>()([^]*?)<\/code><\/pre>/g
|
||||||
const codeblocksWithSyntaxHighlight = /<pre><code class="language-(.*?)">((.|\n)*?)<\/code><\/pre>/g
|
const codeblocksWithSyntaxHighlight = /<(pre><code class)="language-(.*?)">([^]*?)<\/code><\/pre>/g
|
||||||
const inlineCode = /<code>(.*?)<\/code>/g
|
const inlineCode = /<(code)>()(.*?)<\/code>/g
|
||||||
const emailAddresses = /<a href="mailto:(.*?)">((.|\n)*?)<\/a>/g
|
const emailAddresses = /<a href="(mailto):(.*?)">([^]*?)<\/a>/g
|
||||||
const hyperlinks = /<a href="(.*?)">((.|\n)*?)<\/a>/g
|
const mentions = /<a href="https:\/\/(matrix\.to)\/#\/(@.+?)">(.*?)<\/a>/g
|
||||||
|
const hyperlinks = /<(a href)="(.*?)">([^]*?)<\/a>/g
|
||||||
|
const REGEX_CAPTURE_GROUP_COUNT = 3
|
||||||
|
|
||||||
const linebreaks = /<br(.*?)>(\n)?/g
|
RegExp.any = function(...regexes) {
|
||||||
|
let components = []
|
||||||
|
for (const regex of regexes) {
|
||||||
|
if (regex instanceof RegExp) {
|
||||||
|
components = components.concat(regex._components || regex.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RegExp(`(?:${components.join(")|(?:")})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexMonster = RegExp.any(//"g",
|
||||||
|
boldText, italicText, codeblocks, codeblocksWithSyntaxHighlight,
|
||||||
|
inlineCode, emailAddresses, mentions, hyperlinks)
|
||||||
|
const NUMBER_OF_REGEXES_EATEN_BY_MONSTER = 8
|
||||||
|
|
||||||
|
function regexMonsterMatchParser(match) {
|
||||||
|
match.pop() // Remove full string
|
||||||
|
const index = match.pop()
|
||||||
|
let identifier, arg, text
|
||||||
|
for (let i = 0; i < NUMBER_OF_REGEXES_EATEN_BY_MONSTER; i++) {
|
||||||
|
if (match[i * REGEX_CAPTURE_GROUP_COUNT]) {
|
||||||
|
identifier = match[i * REGEX_CAPTURE_GROUP_COUNT]
|
||||||
|
arg = match[(i * REGEX_CAPTURE_GROUP_COUNT) + 1]
|
||||||
|
text = match[(i * REGEX_CAPTURE_GROUP_COUNT) + 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { index, identifier, arg, text }
|
||||||
|
}
|
||||||
|
|
||||||
|
function regexMonsterHandler(identifier, arg, text, index, app) {
|
||||||
|
let entity, entityClass, argField
|
||||||
|
switch (identifier) {
|
||||||
|
case "strong":
|
||||||
|
entityClass = "Bold"
|
||||||
|
break
|
||||||
|
case "em":
|
||||||
|
entityClass = "Italic"
|
||||||
|
break
|
||||||
|
case "pre><code":
|
||||||
|
case "pre><code class":
|
||||||
|
argField = "language"
|
||||||
|
entityClass = "Pre"
|
||||||
|
break
|
||||||
|
case "code":
|
||||||
|
entityClass = "Code"
|
||||||
|
break
|
||||||
|
case "mailto":
|
||||||
|
entityClass = "email"
|
||||||
|
// Force text to be the email address
|
||||||
|
text = arg
|
||||||
|
break
|
||||||
|
case "a href":
|
||||||
|
if (arg === text) {
|
||||||
|
entityClass = "Url"
|
||||||
|
} else {
|
||||||
|
entityClass = "TextUrl"
|
||||||
|
argField = "url"
|
||||||
|
}
|
||||||
|
case "matrix.to":
|
||||||
|
if (app) {
|
||||||
|
const match = app.usernameRegex.exec(arg)
|
||||||
|
if (!match || match.length < 2) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const userID = match[1]
|
||||||
|
|
||||||
|
const user = app.telegramUsersByID.get(+userID)
|
||||||
|
if (!user) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.username) {
|
||||||
|
entityClass = "Mention"
|
||||||
|
text = `@${user.username}`
|
||||||
|
} else {
|
||||||
|
text = user.getDisplayName()
|
||||||
|
entity = {
|
||||||
|
_: "inputMessageEntityMentionName",
|
||||||
|
offset: index,
|
||||||
|
length: text.length,
|
||||||
|
user_id: {
|
||||||
|
_: "inputUser",
|
||||||
|
user_id: user.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!entity && entityClass) {
|
||||||
|
entity = {
|
||||||
|
_: `messageEntity${entityClass}`,
|
||||||
|
offset: index,
|
||||||
|
length: text.length,
|
||||||
|
}
|
||||||
|
if (argField) {
|
||||||
|
entity[argField] = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { replacement: text, entity }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Matrix HTML-formatted message to a Telegram entity-formatted message.
|
* Convert a Matrix HTML-formatted message to a Telegram entity-formatted message.
|
||||||
@@ -124,86 +263,41 @@ const linebreaks = /<br(.*?)>(\n)?/g
|
|||||||
* @param {string} message The HTML-formatted message.
|
* @param {string} message The HTML-formatted message.
|
||||||
* @returns {{message: string, entities: Array}} The Telegram entity-formatted message.
|
* @returns {{message: string, entities: Array}} The Telegram entity-formatted message.
|
||||||
*/
|
*/
|
||||||
function matrixToTelegram(message) {
|
function matrixToTelegram(message, app) {
|
||||||
const entities = []
|
const entities = []
|
||||||
|
|
||||||
|
// First replace all the things that don't get converted into Telegram entities
|
||||||
message = message.replace(linebreaks, "\n")
|
message = message.replace(linebreaks, "\n")
|
||||||
message = message.replace(paragraphs, "$1\n")
|
message = message.replace(paragraphs, "$1\n")
|
||||||
message = message.replace(headers, (_, count, text) => `${"#".repeat(count)} ${text}`)
|
message = message.replace(headers, (_, count, text) => `${"#".repeat(count)} ${text}`)
|
||||||
message = message.replace(unorderedLists, (_, list) => {
|
message = message.replace(unorderedLists, (_, list) => list.replace(listEntries, "- $1"))
|
||||||
return list.replace(listEntries, "- $1")
|
|
||||||
})
|
|
||||||
message = message.replace(orderedLists, (_, list) => {
|
message = message.replace(orderedLists, (_, list) => {
|
||||||
let n = 0
|
let n = 0
|
||||||
return list.replace(listEntries, (fullMatch, text) => `${++n}. ${text}`)
|
return list.replace(listEntries, (fullMatch, text) => `${++n}. ${text}`)
|
||||||
})
|
})
|
||||||
message = message.replace(boldText, (_, text, index) => {
|
|
||||||
entities.push({
|
const regexMonsterReplacer = (match, ...args) => {
|
||||||
_: "messageEntityBold",
|
const { index, identifier, arg, text } = regexMonsterMatchParser(args)
|
||||||
offset: index,
|
if (!identifier) {
|
||||||
length: text.length,
|
// This shouldn't happen
|
||||||
})
|
console.warn(`Warning: Match found but parsing failed for match "${match}"`)
|
||||||
return text
|
return match
|
||||||
})
|
|
||||||
message = message.replace(italicText, (_, text, index) => {
|
|
||||||
entities.push({
|
|
||||||
_: "messageEntityItalic",
|
|
||||||
offset: index,
|
|
||||||
length: text.length,
|
|
||||||
})
|
|
||||||
return text
|
|
||||||
})
|
|
||||||
message = message.replace(codeblocks, (_, text, index) => {
|
|
||||||
entities.push({
|
|
||||||
_: "messageEntityPre",
|
|
||||||
offset: index,
|
|
||||||
length: text.length,
|
|
||||||
language: "",
|
|
||||||
})
|
|
||||||
return text
|
|
||||||
})
|
|
||||||
message = message.replace(codeblocksWithSyntaxHighlight, (_, language, text, index) => {
|
|
||||||
entities.push({
|
|
||||||
_: "messageEntityPre",
|
|
||||||
offset: index,
|
|
||||||
length: text.length,
|
|
||||||
language,
|
|
||||||
})
|
|
||||||
return text
|
|
||||||
})
|
|
||||||
message = message.replace(inlineCode, (_, text, index) => {
|
|
||||||
entities.push({
|
|
||||||
_: "messageEntityCode",
|
|
||||||
offset: index,
|
|
||||||
length: text.length,
|
|
||||||
})
|
|
||||||
return text
|
|
||||||
})
|
|
||||||
message = message.replace(emailAddresses, (_, address, text, index) => {
|
|
||||||
entities.push({
|
|
||||||
_: "messageEntityEmail",
|
|
||||||
offset: index,
|
|
||||||
length: address.length,
|
|
||||||
})
|
|
||||||
return address
|
|
||||||
})
|
|
||||||
message = message.replace(hyperlinks, (_, url, text, index) => {
|
|
||||||
if (url === text) {
|
|
||||||
entities.push({
|
|
||||||
_: "messageEntityUrl",
|
|
||||||
offset: index,
|
|
||||||
length: text.length,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
entities.push({
|
|
||||||
_: "messageEntityTextUrl",
|
|
||||||
offset: index,
|
|
||||||
length: text.length,
|
|
||||||
url,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return text
|
const { replacement, entity } = regexMonsterHandler(identifier, arg, text, index, app)
|
||||||
})
|
if (entity) {
|
||||||
console.log(entities)
|
entities.push(entity)
|
||||||
|
}
|
||||||
|
return replacement || text
|
||||||
|
}
|
||||||
|
|
||||||
|
// We replace matches iteratively to make sure the indexes of matches are correct.
|
||||||
|
let oldMessage = message
|
||||||
|
message = message.replace(regexMonster, regexMonsterReplacer)
|
||||||
|
while (oldMessage !== message) {
|
||||||
|
oldMessage = message
|
||||||
|
message = message.replace(regexMonster, regexMonsterReplacer)
|
||||||
|
}
|
||||||
|
|
||||||
return { message, entities }
|
return { message, entities }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -218,7 +218,7 @@ class Portal {
|
|||||||
|
|
||||||
if (evt.text && evt.text.length > 0) {
|
if (evt.text && evt.text.length > 0) {
|
||||||
if (evt.entities) {
|
if (evt.entities) {
|
||||||
evt.html = formatter.telegramToMatrix(evt.text, evt.entities)
|
evt.html = formatter.telegramToMatrix(evt.text, evt.entities, this.app)
|
||||||
sender.sendHTML(this.roomID, evt.html)
|
sender.sendHTML(this.roomID, evt.html)
|
||||||
} else {
|
} else {
|
||||||
sender.sendText(this.roomID, evt.text)
|
sender.sendText(this.roomID, evt.text)
|
||||||
@@ -252,7 +252,7 @@ class Portal {
|
|||||||
switch (evt.content.msgtype) {
|
switch (evt.content.msgtype) {
|
||||||
case "m.text":
|
case "m.text":
|
||||||
if (evt.content.format === "org.matrix.custom.html") {
|
if (evt.content.format === "org.matrix.custom.html") {
|
||||||
const { message, entities } = formatter.matrixToTelegram(evt.content.formatted_body)
|
const { message, entities } = formatter.matrixToTelegram(evt.content.formatted_body, this.app)
|
||||||
await sender.telegramPuppet.sendMessage(this.peer, message, entities)
|
await sender.telegramPuppet.sendMessage(this.peer, message, entities)
|
||||||
} else {
|
} else {
|
||||||
await sender.telegramPuppet.sendMessage(this.peer, evt.content.body)
|
await sender.telegramPuppet.sendMessage(this.peer, evt.content.body)
|
||||||
|
|||||||
Reference in New Issue
Block a user