parse basic markdown for sending images

This commit is contained in:
HF 2022-03-16 20:51:26 +01:00
parent 91a71c79b2
commit f110d03fdc
8 changed files with 390 additions and 32 deletions

BIN
avatars/ca.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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:

View File

@ -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"

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}
/*