diff --git a/avatars/ca.png b/avatars/ca.png new file mode 100644 index 0000000..292ac88 Binary files /dev/null and b/avatars/ca.png differ diff --git a/ppfun-bridge/README.md b/ppfun-bridge/README.md index 4a46736..69a730b 100644 --- a/ppfun-bridge/README.md +++ b/ppfun-bridge/README.md @@ -26,8 +26,9 @@ app_service_config_files: ``` Edit ecosystem.yml and set the path to the `ppfun-registration.yml` as REGISTRATION_YAML. -Set the pixelplanet APISOCKET_KEY and APISOCKET_URL (like `https://pixelplanet.fun/mcws`) and PPFUN_UID to a user-id from pixelplanet that the bridge sends messages as (can be any number, but its better if its an existing user made for the bridge). +Set the pixelplanet APISOCKET_KEY and APISOCKET_URL (like `https://pixelplanet.fun/mcws`). HOMESERVER_URL should be the local url to matrix-synapse like `http://localhost:8008` and HOMESERVER_DOMAIN its base_url / server_name like `pixelplanet.fun` +MEDIA_URL is the http[s] url from which the matrix server is reachable from the outside, which is usually the base_url, it is needed to send links Now you can start the brige with pm2: diff --git a/ppfun-bridge/ecosystem.yml b/ppfun-bridge/ecosystem.yml index 5d84329..5d1d3fd 100644 --- a/ppfun-bridge/ecosystem.yml +++ b/ppfun-bridge/ecosystem.yml @@ -9,3 +9,4 @@ apps: REGISTRATION_YAML: "/etc/matrix-synapse/ppfun-registration.yaml" HOMESERVER_URL: "http://localhost:8008" HOMESERVER_DOMAIN: "pixelplanet.fun" + MEDIA_URL: "https://matrix.pixelplanet.fun" diff --git a/ppfun-bridge/index.js b/ppfun-bridge/index.js index 18567ab..57574dd 100644 --- a/ppfun-bridge/index.js +++ b/ppfun-bridge/index.js @@ -8,19 +8,19 @@ import PPfunMatrixBridge from './src/ppfunMatrixBridge.js'; const PORT = parseInt(process.env.PORT, 10) || 8009; const APISOCKET_KEY = process.env.APISOCKET_KEY || ''; const APISOCKET_URL = process.env.APISOCKET_URL || 'wss://dev.pixelplanet.fun/mcws'; -const PPFUN_UID = parseInt(process.env.PPFUN_UID, 10) || 1; const REGISTRATION_YAML = process.env.REGISTRATION_YAML || '/etc/matrix-synapse/ppfun-registration.yaml'; const HOMESERVER_URL = process.env.HOMESERVER_URL || 'http://localhost:8008'; const HOMESERVER_DOMAIN = process.env.HOMESERVER_DOMAIN || 'pixelplanet.fun'; +const MEDIA_URL = process.env.MEDIA_URL || `https://${HOMESERVER_DOMAIN}`; const lmao = new PPfunMatrixBridge({ apiSocketKey: APISOCKET_KEY, apiSocketUrl: APISOCKET_URL, - ppfunId: PPFUN_UID, homeserverUrl: HOMESERVER_URL, domain: HOMESERVER_DOMAIN, registration: REGISTRATION_YAML, port: PORT, + mediaUrl: MEDIA_URL }); lmao.run(); diff --git a/ppfun-bridge/src/markdown/MString.js b/ppfun-bridge/src/markdown/MString.js new file mode 100644 index 0000000..498bc6e --- /dev/null +++ b/ppfun-bridge/src/markdown/MString.js @@ -0,0 +1,204 @@ +/* + * class for string iterations + * that is used by MarkdownParser.js + */ + +export default class MString { + constructor(text, start) { + this.txt = text; + this.iter = start || 0; + } + + done() { + return (this.iter >= this.txt.length); + } + + moveForward() { + this.iter += 1; + return (this.iter < this.txt.length); + } + + setIter(iter) { + this.iter = iter; + } + + getChar() { + return this.txt[this.iter]; + } + + slice(start, end) { + return this.txt.slice(start, end || this.iter); + } + + has(str) { + return this.txt.startsWith(str, this.iter); + } + + move(cnt) { + this.iter += cnt; + return (this.iter < this.txt.length); + } + + static isWhiteSpace(chr) { + return (chr === ' ' || chr === '\t' || chr === '\n'); + } + + /* + * check if the current '[' is part of a [y](z) enclosure + * returns [y, z] if it is enclosure, null otherwise + * moves iter to last closing braked if it is enclosure + */ + checkIfEnclosure(zIsLink) { + const yStart = this.iter + 1; + + let yEnd = yStart; + while (this.txt[yEnd] !== ']') { + const chr = this.txt[yEnd]; + if (yEnd >= this.txt.length + || chr === '\n' + || chr === '[' + || chr === '(' + ) { + return null; + } + yEnd += 1; + } + + let zStart = yEnd + 1; + if (this.txt[zStart] !== '(') { + return null; + } + zStart += 1; + + let zEnd = zStart; + let z = null; + while (this.txt[zEnd] !== ')') { + const chr = this.txt[zEnd]; + if (zEnd >= this.txt.length + || chr === '\n' + || chr === '[' + || chr === '(' + ) { + return null; + } + if (zIsLink && chr === ':') { + // set this.iter temporarily to be able to use thischeckIfLink + const oldIter = this.iter; + this.iter = zEnd; + z = this.checkIfLink(); + zEnd = this.iter; + this.iter = oldIter; + if (z === null) { + return null; + } + continue; + } + zEnd += 1; + } + if (zEnd < zStart + 1 || (!z && zIsLink)) { + return null; + } + + if (!zIsLink) { + z = this.txt.slice(zStart, zEnd); + } + const y = this.txt.slice(yStart, yEnd); + + this.iter = zEnd; + return [y, z]; + } + + /* + * Convoluted way to check if the current ':' is part of a link + * we do not check for a 'http' because we might support application links + * like tg://... or discord://.. + * returns the link or false if there is none + * moves iter forward to after the link, if there's one + */ + checkIfLink() { + let cIter = this.iter; + if (!this.txt.startsWith('://', cIter) || cIter < 3) { + return null; + } + + let linkStart = cIter - 1; + for (; linkStart >= 0 + && !MString.isWhiteSpace(this.txt[linkStart]) + && this.txt[linkStart] !== '('; linkStart -= 1); + linkStart += 1; + + cIter += 3; + for (; cIter < this.txt.length + && !MString.isWhiteSpace(this.txt[cIter]) + && this.txt[cIter] !== ')'; cIter += 1 + ); + if (cIter < this.iter + 4) { + return null; + } + + /* special case where someone pasted a http link after a text + * without space in between + */ + let link = this.txt.slice(linkStart, cIter); + const httpOc = link.indexOf('http'); + if (httpOc !== -1 && httpOc !== 0) { + linkStart += httpOc; + link = this.txt.slice(linkStart, cIter); + } + + this.iter = cIter; + return link; + } + + /* + * Check if current '#' is part of ppfun coordinates (example: #d,23,11,-10) + * @return null if not coords, otherwise the coords string + */ + checkIfCoords() { + let cIter = this.iter + 1; + while (cIter < this.txt.length) { + const chr = this.txt[cIter]; + if (chr === ',') { + break; + } + if (MString.isWhiteSpace(chr) + || !Number.isNaN(Number(chr)) + ) { + return null; + } + cIter += 1; + } + if (cIter >= this.txt.length + || cIter - this.iter > 12 + || cIter === this.iter + ) { + return null; + } + cIter += 1; + const curChr = this.txt[cIter]; + if (curChr !== '-' && Number.isNaN(curChr)) { + return null; + } + cIter += 1; + let sectCount = 1; + while (cIter < this.txt.length && !MString.isWhiteSpace(this.txt[cIter])) { + const chr = this.txt[cIter]; + if (chr === ',') { + sectCount += 1; + } else if (chr !== '-' && Number.isNaN(Number(chr))) { + return null; + } + cIter += 1; + } + if (sectCount < 2 + || sectCount > 3 + || this.txt[cIter - 1] === ',' + ) { + return null; + } + + const coords = this.txt.slice(this.iter, cIter); + this.iter = cIter; + return coords; + } +} diff --git a/ppfun-bridge/src/markdown/MarkdownParserLight.js b/ppfun-bridge/src/markdown/MarkdownParserLight.js new file mode 100644 index 0000000..e5f3d04 --- /dev/null +++ b/ppfun-bridge/src/markdown/MarkdownParserLight.js @@ -0,0 +1,104 @@ +/* + * Markdown parsing + * + * Parses the very basics that are needed for the bridge + */ + +import MString from './MString.js'; + +function parseMParagraph(text) { + const pArray = []; + let pStart = text.iter; + let chr = null; + while (!text.done()) { + chr = text.getChar(); + + if (chr === '\n') { + text.moveForward(); + break; + } + + if (chr === '\\') { + /* + * escape character + */ + if (pStart !== text.iter) { + pArray.push(text.slice(pStart)); + } + pStart = text.iter + 1; + text.moveForward(); + } else if (chr === '#') { + /* + * ppfun coords #d,34,23,-10 + */ + const oldPos = text.iter; + const coords = text.checkIfCoords(); + if (coords) { + if (pStart !== oldPos) { + pArray.push(text.slice(pStart, oldPos)); + } + pArray.push(['l', null, `https://pixelplanet.fun/${coords}`]); + pStart = text.iter; + } + } else if (chr === ':') { + /* + * pure link + */ + const link = text.checkIfLink(); + if (link !== null) { + const startLink = text.iter - link.length; + if (pStart < startLink) { + pArray.push(text.slice(pStart, startLink)); + } + pArray.push(['l', null, link]); + pStart = text.iter; + continue; + } + } else if (chr === '[') { + /* + * x[y](z) enclosure + */ + let oldPos = text.iter; + let x = null; + if (text.iter > 0) { + text.move(-1); + x = text.getChar(); + text.setIter(oldPos); + } + /* + * x decides what element it is + * defaults to ordinary link + */ + let tag = 'l'; + let zIsLink = true; + if (x === '!') { + tag = 'img'; + oldPos -= 1; + } else if (x === '@') { + zIsLink = false; + tag = '@'; + oldPos -= 1; + } + + const encArr = text.checkIfEnclosure(zIsLink); + if (encArr !== null) { + if (pStart < oldPos) { + pArray.push(text.slice(pStart, oldPos)); + } + pArray.push([tag, encArr[0], encArr[1]]); + pStart = text.iter + 1; + } + } + + text.moveForward(); + } + if (pStart !== text.iter) { + pArray.push(text.slice(pStart)); + } + return pArray; +} + +export function parseParagraph(text,) { + const mText = new MString(text); + return parseMParagraph(mText); +} diff --git a/ppfun-bridge/src/markdown/parse.js b/ppfun-bridge/src/markdown/parse.js new file mode 100644 index 0000000..cfa0285 --- /dev/null +++ b/ppfun-bridge/src/markdown/parse.js @@ -0,0 +1,28 @@ +import { parseParagraph } from './MarkdownParserLight.js'; + +export default function parseMsg(msg) { + const pArray = parseParagraph(msg); + let output = ''; + for (let i = 0; i < pArray.length; i += 1) { + const part = pArray[i]; + if (!Array.isArray(part)) { + output += part; + } else { + const type = part[0]; + switch (type) { + case '@': { + output += `@${part[1]}`; + break; + } + case 'img': + case 'l': { + output += part[2]; + break; + } + default: + output += type; + } + } + } + return output; +} diff --git a/ppfun-bridge/src/ppfunMatrixBridge.js b/ppfun-bridge/src/ppfunMatrixBridge.js index 392ee70..9b0e2d3 100644 --- a/ppfun-bridge/src/ppfunMatrixBridge.js +++ b/ppfun-bridge/src/ppfunMatrixBridge.js @@ -2,6 +2,7 @@ import { Bridge } from 'matrix-appservice-bridge'; +import parseMsg from './markdown/parse.js' import PPfunSocket from './ppfunsocket.js'; import { parseCanvasLinks } from './pixelplanet/index.js'; @@ -11,6 +12,7 @@ class PPfunMatrixBridge { apiSocketKey, apiSocketUrl, homeserverUrl, + mediaUrl, domain, registration, port @@ -39,6 +41,7 @@ class PPfunMatrixBridge { }); this.port = port; this.domain = domain; + this.mediaUrl = mediaUrl; this.prefix = 'pp'; this.ppfunSocket.on('chanList', this.connectRooms.bind(this)); @@ -69,12 +72,13 @@ class PPfunMatrixBridge { console.warn(`Dropping a message because channels aren't synced yet`); return; } + const parsedMsg = parseMsg(msg); this.echoSuppression.set(`${uid}:${cid}`, Date.now()); - console.log(`PPFUN ${name}: ${msg}`); + console.log(`PPFUN ${name}: ${parsedMsg}`); this.sendMatrix( name, `@${this.prefix}_${uid}:${this.domain}`, - msg, + parsedMsg, matrixRoom, ); } @@ -112,33 +116,7 @@ class PPfunMatrixBridge { async recMatrix(request, context) { const event = request.getData(); - if (event.type === "m.room.message" - && event.content - && event.content.msgtype === "m.text" - ) { - const cid = this.mToProomMap.get(event.room_id); - if (!cid) { - return; - } - const msg = event.content.body; - this.sendCanvasSnapshotIfNeccessary(event.room_id, msg); - const userId = event.sender; - const uid = (userId.startsWith(`@${this.prefix}_`) - && userId.endsWith(this.domain)) - ? userId.slice(2 + this.prefix.length, -this.domain.length - 1) - : null; - if (this.echoSuppression.delete(`${uid}:${cid}`)) { - return; - } - let name = this.idToNameMap.get(userId); - if (!name) { - this.idToNameMap.set(userId, userId.substring(1)); - name = await this.getDisplayName(userId); - } - console.log(`MATRIX ${name}: ${msg}`); - this.sendPPfun(name, parseInt(uid, 10), msg, cid); - return; - } + // user joined or set displayname if (event.type === "m.room.member" && event.user_id && event.content @@ -149,6 +127,48 @@ class PPfunMatrixBridge { console.log(`User ${userId} joined or set displayname to ${name}`); this.idToNameMap.set(userId, name); } + // Only room messages from bridged rooms past this point + if (event.type !== "m.room.message" || !event.content) { + return; + } + const cid = this.mToProomMap.get(event.room_id); + if (!cid) { + return; + } + let msg; + // Media: send url as markdown + if ((event.content.msgtype === "m.image" + || event.content.msgtype === "m.video") + && event.content.url + ) { + // adding a query parameter so that client can guess what it is + const url = `${this.mediaUrl}/_matrix/media/r0/download/${event.content.url.substring("mxc://".length)}?type=${event.content.msgtype.substring(2)}` + msg = `[${event.content.body}](${url})`; + // Text + } else if (event.content.msgtype === "m.text") { + msg = event.content.body; + this.sendCanvasSnapshotIfNeccessary(event.room_id, msg); + } + if (!msg) { + return; + } + + const userId = event.sender; + const uid = (userId.startsWith(`@${this.prefix}_`) + && userId.endsWith(this.domain)) + ? userId.slice(2 + this.prefix.length, -this.domain.length - 1) + : null; + if (this.echoSuppression.delete(`${uid}:${cid}`)) { + return; + } + let name = this.idToNameMap.get(userId); + if (!name) { + this.idToNameMap.set(userId, userId.substring(1)); + name = await this.getDisplayName(userId); + } + console.log(`MATRIX ${name}: ${msg}`); + this.sendPPfun(name, parseInt(uid, 10), msg, cid); + return; } /*