Socket
Socket
Sign inDemoInstall

mx-puppet-bridge

Package Overview
Dependencies
293
Maintainers
1
Versions
112
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.0.45 to 0.1.0-1

27

lib/src/botprovisioner.js

@@ -522,3 +522,3 @@ "use strict";

}),
help: `Sets if the given puppet is public.
help: `Sets if the given puppet creates rooms as public or invite-only.

@@ -534,3 +534,3 @@ Usage: \`setispublic <puppetId> <1/0>`,

}),
help: `Sets if the given puppet is public.
help: `Sets if the given puppet creates shared or separate rooms for multiple users accessing the same bridged room.

@@ -546,3 +546,3 @@ Usage: \`setisglobalnamespace <puppetId> <1/0>\``,

}),
help: `Sets if the given puppet should autoinvite you to new rooms.
help: `Sets if the given puppet should autoinvite you to newly bridged rooms.

@@ -647,2 +647,23 @@ Usage: \`setautoinvite <puppetId> <1/0>`,

});
this.registerCommand("fixmute", {
fn: (sender, param, sendMessage, roomId) => __awaiter(this, void 0, void 0, function* () {
const roomParts = yield this.bridge.roomSync.resolve(roomId || param);
if (!roomParts) {
yield sendMessage("Room not resolvable");
return;
}
const room = yield this.bridge.roomSync.maybeGet(roomParts);
if (!room) {
yield sendMessage("Room not found");
return;
}
yield sendMessage("Fixing muted user...");
yield this.provisioner.adjustMuteIfInRoom(sender, room.mxid);
}),
help: `Fix the power levels according to puppet & relay availability in the bridged room.
Usage: \`fixmute <room resolvable>\``,
withPid: false,
inRoom: true,
});
this.registerCommand("resendbridgeinfo", {

@@ -649,0 +670,0 @@ fn: (sender, param, sendMessage) => __awaiter(this, void 0, void 0, function* () {

@@ -10,2 +10,3 @@ import { IDatabaseConnector } from "./connector";

newData(mxid: string, roomId: string, puppetId: number): IRoomStoreEntry;
getAll(): Promise<IRoomStoreEntry[]>;
getByRemote(puppetId: number, roomId: string): Promise<IRoomStoreEntry | null>;

@@ -12,0 +13,0 @@ getByPuppetId(puppetId: number): Promise<IRoomStoreEntry[]>;

@@ -46,2 +46,15 @@ "use strict";

}
getAll() {
return __awaiter(this, void 0, void 0, function* () {
const rows = yield this.db.All("SELECT * FROM room_store");
const results = [];
for (const row of rows) {
const res = this.getFromRow(row);
if (res) {
results.push(res);
}
}
return results;
});
}
getByRemote(puppetId, roomId) {

@@ -48,0 +61,0 @@ return __awaiter(this, void 0, void 0, function* () {

/// <reference types="node" />
import { IStringFormatterVars } from "./structures/stringformatter";
import { MessageEvent, TextualMessageEventContent, FileMessageEventContent } from "@sorunome/matrix-bot-sdk";
export declare type MatrixPresence = "offline" | "online" | "unavailable";
declare type PuppetDataSingleType = string | number | boolean | IPuppetData | null | undefined;

@@ -88,2 +89,8 @@ export interface IPuppetData {

}
export interface IPresenceEvent {
currentlyActive?: boolean;
lastActiveAgo?: number;
presence: MatrixPresence;
statusMsg?: string;
}
export interface IEventInfo {

@@ -90,0 +97,0 @@ message?: IMessageEvent;

@@ -7,2 +7,3 @@ import { PuppetBridge } from "./puppetbridge";

private memberInfoCache;
private typingCache;
constructor(bridge: PuppetBridge);

@@ -24,2 +25,5 @@ registerAppserviceEvents(): void;

private handleRoomQuery;
private handlePresence;
private handleTyping;
private handleReceipt;
private getRoomDisplaynameCache;

@@ -26,0 +30,0 @@ private updateCachedRoomMemberInfo;

@@ -35,2 +35,3 @@ "use strict";

this.bridge = bridge;
this.typingCache = new Map();
this.memberInfoCache = {};

@@ -66,2 +67,16 @@ }

}));
// tslint:disable-next-line no-any
this.bridge.AS.on("ephemeral.event", (rawEvent) => __awaiter(this, void 0, void 0, function* () {
switch (rawEvent.type) {
case "m.presence":
yield this.handlePresence(rawEvent);
break;
case "m.typing":
yield this.handleTyping(rawEvent.room_id, rawEvent);
break;
case "m.receipt":
yield this.handleReceipt(rawEvent.room_id, rawEvent);
break;
}
}));
}

@@ -183,2 +198,8 @@ getEventInfo(roomId, eventId, client, sender) {

log.info(`Got new user join event from ${userId} in ${roomId}...`);
try {
yield this.bridge.provisioner.adjustMute(userId, roomId);
}
catch (err) {
log.error("Error checking mute status", err.error || err.body || err);
}
const room = yield this.getRoomParts(roomId, event.sender);

@@ -576,2 +597,165 @@ if (!room) {

}
// tslint:disable-next-line no-any
handlePresence(rawEvent) {
return __awaiter(this, void 0, void 0, function* () {
if (this.bridge.AS.isNamespacedUser(rawEvent.sender)) {
return; // we don't handle our own namespace
}
log.info(`Got presence event for mxid ${rawEvent.sender}`);
const puppetDatas = yield this.bridge.provisioner.getForMxid(rawEvent.sender);
let puppetData = null;
for (const p of puppetDatas) {
if (p.type === "puppet") {
puppetData = p;
break;
}
}
if (!puppetData) {
const allPuppets = yield this.bridge.provisioner.getAll();
const allRelays = allPuppets.filter((p) => p.type === "relay" && p.isGlobalNamespace);
if (allRelays.length > 0) {
puppetData = allRelays[0];
}
}
if (!puppetData) {
log.error("Puppet not found. Something is REALLY wrong!!!!");
return;
}
if (puppetData.type === "relay") {
if (!this.bridge.protocol.features.advancedRelay) {
log.verbose("Simple relays can't have foreign presences, dropping...");
return;
}
if (!this.bridge.provisioner.canRelay(rawEvent.sender)) {
log.verbose("Presence wasn't sent from a relay-able person, dropping...");
return;
}
}
else if (rawEvent.sender !== puppetData.puppetMxid) {
log.verbose("Presence wasn't sent from the correct puppet, dropping...");
return;
}
const asUser = yield this.getSendingUser(puppetData, "", rawEvent.sender);
const presence = {
currentlyActive: rawEvent.content.currently_active,
lastActiveAgo: rawEvent.content.last_active_ago,
presence: rawEvent.content.presence,
statusMsg: rawEvent.content.status_msg,
};
log.verbose("Emitting presence event...");
this.bridge.emit("presence", puppetData.puppetId, presence, asUser, rawEvent);
});
}
// tslint:disable-next-line no-any
handleTyping(roomId, rawEvent) {
return __awaiter(this, void 0, void 0, function* () {
let first = true;
const pastTypingSet = this.typingCache.get(roomId) || new Set();
const newTypingSet = new Set(rawEvent.content.user_ids);
const changeUserIds = new Map();
// first determine all the typing stops
for (const pastUserId of pastTypingSet) {
if (!newTypingSet.has(pastUserId)) {
changeUserIds.set(pastUserId, false);
}
}
// now determine all typing starts
for (const newUserId of newTypingSet) {
if (!pastTypingSet.has(newUserId)) {
changeUserIds.set(newUserId, true);
}
}
this.typingCache.set(roomId, newTypingSet);
for (const [userId, typing] of changeUserIds) {
if (this.bridge.AS.isNamespacedUser(userId)) {
continue; // we don't handle our own namespace
}
if (first) {
log.info(`Got typing event in room ${roomId}`);
first = false;
}
const room = yield this.getRoomParts(roomId, userId);
if (!room) {
log.verbose("Room not found, ignoring...");
continue;
}
const puppetData = yield this.bridge.provisioner.get(room.puppetId);
if (!puppetData) {
log.error("Puppet not found. Something is REALLY wrong!!!");
continue;
}
if (puppetData.type === "relay") {
if (!this.bridge.protocol.features.advancedRelay) {
log.verbose("Simple relays can't have foreign typing, dropping...");
continue;
}
if (!this.bridge.provisioner.canRelay(userId)) {
log.verbose("Typing wasn't sent from a relay-able person, dropping...");
continue;
}
}
else if (userId !== puppetData.puppetMxid) {
log.verbose("Typing wasn't sent from the correct puppet, dropping...");
continue;
}
const asUser = yield this.getSendingUser(puppetData, roomId, userId);
log.verbose("Emitting typing event...");
this.bridge.emit("typing", room, typing, asUser, rawEvent);
}
});
}
// tslint:disable-next-line no-any
handleReceipt(roomId, rawEvent) {
return __awaiter(this, void 0, void 0, function* () {
let first = true;
// tslint:disable-next-line no-any
for (const [eventId, allContents] of Object.entries(rawEvent.content)) {
if (allContents["m.read"]) {
// we have read receipts
// tslint:disable-next-line no-any
for (const [userId, content] of Object.entries(allContents["m.read"])) {
if (this.bridge.AS.isNamespacedUser(userId)) {
continue; // we don't handle our own namespace
}
if (first) {
log.info(`Got receipt event in room ${roomId}`);
first = false;
}
const room = yield this.getRoomParts(roomId, userId);
if (!room) {
log.verbose("Room not found, dropping...");
continue;
}
const event = (yield this.bridge.eventSync.getRemote(room, eventId))[0];
if (!event) {
log.verbose("Event not found, dropping...");
continue;
}
const puppetData = yield this.bridge.provisioner.get(room.puppetId);
if (!puppetData) {
log.error("Puppet not found. Something is REALLY wrong!!!!");
continue;
}
if (puppetData.type === "relay") {
if (!this.bridge.protocol.features.advancedRelay) {
log.verbose("Simple relays can't have foreign receipts, dropping...");
continue;
}
if (!this.bridge.provisioner.canRelay(userId)) {
log.verbose("Receipt wasn't sent from a relay-able person, dropping...");
continue;
}
}
else if (userId !== puppetData.puppetMxid) {
log.verbose("Receipt wasn't sent from the correct puppet, dropping...");
continue;
}
const asUser = yield this.getSendingUser(puppetData, roomId, userId);
log.debug("Emitting read event...");
this.bridge.emit("read", room, event, content, asUser, rawEvent);
}
}
}
});
}
getRoomDisplaynameCache(roomId) {

@@ -663,3 +847,3 @@ if (!(roomId in this.memberInfoCache)) {

}
const membership = yield this.getRoomMemberInfo(roomId, userId);
const membership = roomId ? yield this.getRoomMemberInfo(roomId, userId) : null;
let user = null;

@@ -666,0 +850,0 @@ try {

2

lib/src/presencehandler.d.ts
import { PuppetBridge } from "./puppetbridge";
import { PresenceConfig } from "./config";
export declare type MatrixPresence = "offline" | "online" | "unavailable";
import { MatrixPresence } from "./interfaces";
export declare class PresenceHandler {

@@ -5,0 +5,0 @@ private bridge;

@@ -50,2 +50,6 @@ import { PuppetBridge } from "./puppetbridge";

setAdmin(userId: string, ident: RemoteRoomResolvable): Promise<void>;
adjustMute(userId: string, room: string): Promise<void>;
adjustMuteIfInRoom(userId: string, room: string): Promise<void>;
adjustMuteListRooms(puppetId: number, userId: string): Promise<void>;
adjustMuteEverywhere(userId: string): Promise<void>;
invite(userId: string, ident: RemoteRoomResolvable): Promise<boolean>;

@@ -52,0 +56,0 @@ groupInvite(userId: string, ident: RemoteGroupResolvable): Promise<boolean>;

@@ -200,2 +200,6 @@ "use strict";

this.bridge.emit("puppetNew", puppetId, data);
const WAIT_CONNECT_TIMEOUT = 10000;
setTimeout(() => __awaiter(this, void 0, void 0, function* () {
yield this.adjustMuteListRooms(puppetId, puppetMxid);
}), WAIT_CONNECT_TIMEOUT);
return puppetId;

@@ -230,2 +234,3 @@ });

yield this.puppetStore.delete(puppetId);
yield this.adjustMuteEverywhere(puppetMxid);
this.bridge.emit("puppetDelete", puppetId);

@@ -378,2 +383,85 @@ });

}
adjustMute(userId, room) {
return __awaiter(this, void 0, void 0, function* () {
const MUTED_POWER_LEVEL = -1;
const UNMUTED_POWER_LEVEL = 0;
log.verbose(`Adjusting Mute for ${userId} in ${room}`);
let currentLevel = UNMUTED_POWER_LEVEL;
const client = yield this.bridge.roomSync.getRoomOp(room);
if (!client) {
throw new Error("Failed to get operator of " + room);
}
if (!(yield client.userHasPowerLevelFor(userId, room, "m.room.message", false))) {
currentLevel = MUTED_POWER_LEVEL;
}
let targetLevel = UNMUTED_POWER_LEVEL;
const roomParts = yield this.bridge.roomSync.resolve(room);
if (!roomParts) {
throw new Error("Failed to resolve room " + room);
}
const data = yield this.bridge.roomStore.getByMxid(room);
if (!data) {
throw new Error("Failed to get data for " + room);
return;
}
if (!data || data.isDirect) {
log.verbose(`No muting in direct rooms`);
}
else if (!(yield this.bridge.namespaceHandler.canSeeRoom(roomParts, userId))) {
targetLevel = MUTED_POWER_LEVEL;
}
if (currentLevel !== targetLevel) {
log.verbose(`Adjusting power level of ${userId} in ${room} to ${targetLevel}`);
yield client.setUserPowerLevel(userId, room, targetLevel);
}
});
}
adjustMuteIfInRoom(userId, room) {
return __awaiter(this, void 0, void 0, function* () {
const client = yield this.bridge.roomSync.getRoomOp(room);
if (!client) {
throw new Error("Failed to get operator of " + room);
}
try {
const members = yield client.getJoinedRoomMembers(room);
if (!members.includes(userId)) {
return;
}
}
catch (err) {
log.error("Error getting room members", err.error || err.body || err);
}
yield this.adjustMute(userId, room);
});
}
adjustMuteListRooms(puppetId, userId) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.bridge.hooks.listRooms) {
return;
}
const rooms = yield this.bridge.hooks.listRooms(puppetId);
for (const r of rooms) {
if (!r.id) {
continue;
}
const roomInfo = yield this.bridge.roomSync.maybeGet({
puppetId,
roomId: r.id,
});
if (!roomInfo) {
continue;
}
log.verbose(`roommxid: ${roomInfo.mxid}`);
yield this.adjustMuteIfInRoom(userId, roomInfo.mxid);
}
});
}
adjustMuteEverywhere(userId) {
return __awaiter(this, void 0, void 0, function* () {
const entries = yield this.bridge.roomStore.getAll();
for (const entry of entries) {
yield this.adjustMuteIfInRoom(userId, entry.mxid);
}
});
}
invite(userId, ident) {

@@ -385,5 +473,9 @@ return __awaiter(this, void 0, void 0, function* () {

}
const room = yield this.bridge.roomSync.maybeGet(roomParts);
let room = yield this.bridge.roomSync.maybeGet(roomParts);
if (!room) {
return false;
yield this.bridge.bridgeRoom(roomParts);
room = yield this.bridge.roomSync.maybeGet(roomParts);
if (!room) {
return false;
}
}

@@ -390,0 +482,0 @@ if (yield this.bridge.namespaceHandler.canSeeRoom(room, userId)) {

@@ -20,3 +20,3 @@ /// <reference types="node" />

import { ProvisioningAPI } from "./provisioningapi";
import { PresenceHandler, MatrixPresence } from "./presencehandler";
import { PresenceHandler } from "./presencehandler";
import { TypingHandler } from "./typinghandler";

@@ -26,3 +26,3 @@ import { ReactionHandler } from "./reactionhandler";

import { DelayedFunction } from "./structures/delayedfunction";
import { IPuppetBridgeRegOpts, IPuppetBridgeFeatures, IReceiveParams, IMessageEvent, IProtocolInformation, CreateRoomHook, CreateUserHook, CreateGroupHook, GetDescHook, BotHeaderMsgHook, GetDataFromStrHook, GetDmRoomIdHook, ListUsersHook, ListRoomsHook, ListGroupsHook, IRemoteUser, IRemoteRoom, IRemoteGroup, IPuppetData, GetUserIdsInRoomHook, UserExistsHook, RoomExistsHook, GroupExistsHook, ResolveRoomIdHook, IEventInfo } from "./interfaces";
import { IPuppetBridgeRegOpts, IPuppetBridgeFeatures, IReceiveParams, IMessageEvent, IProtocolInformation, CreateRoomHook, CreateUserHook, CreateGroupHook, GetDescHook, BotHeaderMsgHook, GetDataFromStrHook, GetDmRoomIdHook, ListUsersHook, ListRoomsHook, ListGroupsHook, IRemoteUser, IRemoteRoom, IRemoteGroup, IPuppetData, GetUserIdsInRoomHook, UserExistsHook, RoomExistsHook, GroupExistsHook, ResolveRoomIdHook, IEventInfo, MatrixPresence } from "./interfaces";
export interface IPuppetBridgeHooks {

@@ -29,0 +29,0 @@ createUser?: CreateUserHook;

@@ -195,6 +195,6 @@ "use strict";

const reg = {
as_token: uuid(),
hs_token: uuid(),
id: opts.id,
namespaces: {
"as_token": uuid(),
"hs_token": uuid(),
"id": opts.id,
"namespaces": {
users: [{

@@ -210,6 +210,7 @@ exclusive: true,

},
protocols: [],
rate_limited: false,
sender_localpart: opts.botUser,
url: opts.url,
"protocols": [],
"rate_limited": false,
"sender_localpart": opts.botUser,
"url": opts.url,
"de.sorunome.msc2409.push_ephemeral": true,
};

@@ -269,6 +270,7 @@ fs.writeFileSync(this.registrationPath, yaml.safeDump(reg));

try {
if (displayname) {
const currProfile = yield this.appservice.botIntent.underlyingClient.getUserProfile(this.appservice.botIntent.userId);
if (displayname && displayname !== currProfile.displayname) {
yield this.appservice.botIntent.underlyingClient.setDisplayName(displayname);
}
if (this.config.bridge.avatarUrl) {
if (this.config.bridge.avatarUrl && this.config.bridge.avatarUrl !== currProfile.avatar_url) {
yield this.appservice.botIntent.underlyingClient.setAvatarUrl(this.config.bridge.avatarUrl);

@@ -275,0 +277,0 @@ }

/// <reference types="node" />
import { PuppetBridge } from "./puppetbridge";
import { IRemoteUser, IReceiveParams, IMessageEvent } from "./interfaces";
import { MatrixPresence } from "./presencehandler";
import { IRemoteUser, IReceiveParams, IMessageEvent, MatrixPresence } from "./interfaces";
export declare class RemoteEventHandler {

@@ -6,0 +5,0 @@ private bridge;

@@ -28,2 +28,4 @@ "use strict";

const escapeHtml = require("escape-html");
const blurhash_1 = require("blurhash");
const Canvas = require("canvas");
const log = new log_1.Log("RemoteEventHandler");

@@ -407,2 +409,43 @@ // tslint:disable no-magic-numbers

}
try {
const orientation = yield util_1.Util.getExifOrientation(buffer);
const FIRST_EXIF_ROTATED = 5;
if (orientation > FIRST_EXIF_ROTATED) {
// flip width and height
const tmp = i.w;
i.w = i.h;
i.h = tmp;
}
}
catch (err) {
log.debug("Error fetching exif orientation for image", err);
}
const BLURHASH_CHUNKS = 4;
const image = yield new Promise((resolve, reject) => {
const img = new Canvas.Image();
img.onload = () => resolve(img);
img.onerror = (...args) => reject(args);
img.src = "data:image/png;base64," + buffer.toString("base64");
});
let drawWidth = image.width;
let drawHeight = image.height;
const drawMax = 50;
if (drawWidth > drawMax || drawHeight > drawMax) {
if (drawWidth > drawHeight) {
drawHeight = Math.round(drawMax * (drawHeight / drawWidth));
drawWidth = drawMax;
}
else {
drawWidth = Math.round(drawMax * (drawWidth / drawHeight));
drawHeight = drawMax;
}
}
const canvas = Canvas.createCanvas(drawWidth, drawHeight);
const context = canvas.getContext("2d");
if (context) {
context.drawImage(image, 0, 0, drawWidth, drawHeight);
const blurhashImageData = context.getImageData(0, 0, drawWidth, drawHeight);
// tslint:disable-next-line no-any
i["xyz.amorgan.blurhash"] = blurhash_1.encode(blurhashImageData.data, drawWidth, drawHeight, BLURHASH_CHUNKS, BLURHASH_CHUNKS);
}
}

@@ -409,0 +452,0 @@ catch (err) {

@@ -101,2 +101,3 @@ "use strict";

}
length--; // else we gobble the ] twice
return {

@@ -145,2 +146,5 @@ if: resStrs[SEARCHING_IF],

if (res === result) {
if (!res) {
return "true";
}
return res;

@@ -147,0 +151,0 @@ }

/// <reference types="node" />
import { IProfileDbEntry } from "./db/interfaces";
import { IRemoteProfile } from "./interfaces";
import { OptionsOfDefaultResponseBody } from "got/dist/source/create";
import { OptionsOfBufferResponseBody } from "got";
export interface IMakeUploadFileData {

@@ -11,3 +11,3 @@ avatarUrl?: string | null;

export declare class Util {
static DownloadFile(url: string, options?: OptionsOfDefaultResponseBody): Promise<Buffer>;
static DownloadFile(url: string, options?: OptionsOfBufferResponseBody): Promise<Buffer>;
static GetMimeType(buffer: Buffer): string | undefined;

@@ -26,2 +26,3 @@ static str2mxid(a: string): string;

static ffprobe(buffer: Buffer): Promise<any>;
static getExifOrientation(buffer: Buffer): Promise<number>;
}

@@ -34,3 +34,3 @@ "use strict";

class Util {
static DownloadFile(url, options = {}) {
static DownloadFile(url, options = { responseType: "buffer" }) {
return __awaiter(this, void 0, void 0, function* () {

@@ -266,3 +266,35 @@ if (!options.method) {

}
static getExifOrientation(buffer) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const cmd = child_process_1.spawn("identify", ["-format", "'%[EXIF:Orientation]'", "-"]);
const TIMEOUT = 5000;
const timeout = setTimeout(() => {
cmd.kill();
}, TIMEOUT);
let databuf = "";
cmd.stdout.on("data", (data) => {
databuf += data;
});
cmd.stdout.on("error", (error) => { }); // disregard
cmd.on("error", (error) => {
cmd.kill();
clearTimeout(timeout);
reject(error);
});
cmd.on("close", (code) => {
clearTimeout(timeout);
try {
resolve(Number(databuf.replace(/.*(\d+).*/, "$1")));
}
catch (err) {
reject(err);
}
});
cmd.stdin.on("error", (error) => { }); // disregard
cmd.stdin.end(buffer);
});
});
}
}
exports.Util = Util;
{
"name": "mx-puppet-bridge",
"version": "0.0.45",
"version": "0.1.0-1",
"description": "Matrix Puppeting Bridge library",

@@ -23,4 +23,6 @@ "repository": {

"dependencies": {
"@sorunome/matrix-bot-sdk": "^0.5.3-3",
"@sorunome/matrix-bot-sdk": "^0.5.8",
"better-sqlite3": "^6.0.1",
"blurhash": "^1.1.3",
"canvas": "^2.6.1",
"escape-html": "^1.0.3",

@@ -30,3 +32,3 @@ "events": "^3.1.0",

"file-type": "^12.4.2",
"got": "^10.7.0",
"got": "^11.6.0",
"hasha": "^5.2.0",

@@ -33,0 +35,0 @@ "js-yaml": "^3.13.1",

@@ -84,3 +84,3 @@ [Support Chat](https://matrix.to/#/#mx-puppet-bridge:sorunome.de) [![donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/Sorunome/donate)

[matrix-synapse-secret-auth](https://github.com/devture/matrix-synapse-shared-secret-auth) and set
the secert for that homeserver in the `bridge.loginSharedSecretMap` mapping.
the secret for that homeserver in the `bridge.loginSharedSecretMap` mapping.

@@ -87,0 +87,0 @@ ### Adding of metadata for images, videos and audio

@@ -531,3 +531,3 @@ /*

},
help: `Sets if the given puppet is public.
help: `Sets if the given puppet creates rooms as public or invite-only.

@@ -543,3 +543,3 @@ Usage: \`setispublic <puppetId> <1/0>`,

},
help: `Sets if the given puppet is public.
help: `Sets if the given puppet creates shared or separate rooms for multiple users accessing the same bridged room.

@@ -555,3 +555,3 @@ Usage: \`setisglobalnamespace <puppetId> <1/0>\``,

},
help: `Sets if the given puppet should autoinvite you to new rooms.
help: `Sets if the given puppet should autoinvite you to newly bridged rooms.

@@ -652,2 +652,23 @@ Usage: \`setautoinvite <puppetId> <1/0>`,

});
this.registerCommand("fixmute", {
fn: async (sender: string, param: string, sendMessage: SendMessageFn, roomId?: string) => {
const roomParts = await this.bridge.roomSync.resolve(roomId || param);
if (!roomParts) {
await sendMessage("Room not resolvable");
return;
}
const room = await this.bridge.roomSync.maybeGet(roomParts);
if (!room) {
await sendMessage("Room not found");
return;
}
await sendMessage("Fixing muted user...");
await this.provisioner.adjustMuteIfInRoom(sender, room.mxid);
},
help: `Fix the power levels according to puppet & relay availability in the bridged room.
Usage: \`fixmute <room resolvable>\``,
withPid: false,
inRoom: true,
});
this.registerCommand("resendbridgeinfo", {

@@ -654,0 +675,0 @@ fn: async (sender: string, param: string, sendMessage: SendMessageFn) => {

@@ -48,2 +48,14 @@ /*

public async getAll(): Promise<IRoomStoreEntry[]> {
const rows = await this.db.All("SELECT * FROM room_store");
const results: IRoomStoreEntry[] = [];
for (const row of rows) {
const res = this.getFromRow(row);
if (res) {
results.push(res);
}
}
return results;
}
public async getByRemote(puppetId: number, roomId: string): Promise<IRoomStoreEntry | null> {

@@ -50,0 +62,0 @@ const cached = this.remoteCache.get(`${puppetId};${roomId}`);

@@ -19,2 +19,4 @@ /*

export type MatrixPresence = "offline" | "online" | "unavailable";
type PuppetDataSingleType = string | number | boolean | IPuppetData | null | undefined;

@@ -131,2 +133,9 @@ export interface IPuppetData {

export interface IPresenceEvent {
currentlyActive?: boolean;
lastActiveAgo?: number;
presence: MatrixPresence;
statusMsg?: string;
}
export interface IEventInfo {

@@ -133,0 +142,0 @@ message?: IMessageEvent;

@@ -21,3 +21,3 @@ /*

import {
IFileEvent, IMessageEvent, IRemoteRoom, ISendingUser, IRemoteUser, IReplyEvent, IEventInfo,
IFileEvent, IMessageEvent, IRemoteRoom, ISendingUser, IRemoteUser, IReplyEvent, IEventInfo, IPresenceEvent,
} from "./interfaces";

@@ -36,2 +36,3 @@ import * as escapeHtml from "escape-html";

private memberInfoCache: { [roomId: string]: { [userId: string]: MembershipEventContent } };
private typingCache: Map<string, Set<string>> = new Map();

@@ -69,2 +70,16 @@ constructor(

});
// tslint:disable-next-line no-any
this.bridge.AS.on("ephemeral.event", async (rawEvent: any) => {
switch (rawEvent.type) {
case "m.presence":
await this.handlePresence(rawEvent);
break;
case "m.typing":
await this.handleTyping(rawEvent.room_id, rawEvent);
break;
case "m.receipt":
await this.handleReceipt(rawEvent.room_id, rawEvent);
break;
}
});
}

@@ -188,2 +203,7 @@

log.info(`Got new user join event from ${userId} in ${roomId}...`);
try {
await this.bridge.provisioner.adjustMute(userId, roomId);
} catch (err) {
log.error("Error checking mute status", err.error || err.body || err);
}
const room = await this.getRoomParts(roomId, event.sender);

@@ -592,2 +612,159 @@ if (!room) {

// tslint:disable-next-line no-any
private async handlePresence(rawEvent: any) {
if (this.bridge.AS.isNamespacedUser(rawEvent.sender)) {
return; // we don't handle our own namespace
}
log.info(`Got presence event for mxid ${rawEvent.sender}`);
const puppetDatas = await this.bridge.provisioner.getForMxid(rawEvent.sender);
let puppetData: IPuppet | null = null;
for (const p of puppetDatas) {
if (p.type === "puppet") {
puppetData = p;
break;
}
}
if (!puppetData) {
const allPuppets = await this.bridge.provisioner.getAll();
const allRelays = allPuppets.filter((p) => p.type === "relay" && p.isGlobalNamespace);
if (allRelays.length > 0) {
puppetData = allRelays[0];
}
}
if (!puppetData) {
log.error("Puppet not found. Something is REALLY wrong!!!!");
return;
}
if (puppetData.type === "relay") {
if (!this.bridge.protocol.features.advancedRelay) {
log.verbose("Simple relays can't have foreign presences, dropping...");
return;
}
if (!this.bridge.provisioner.canRelay(rawEvent.sender)) {
log.verbose("Presence wasn't sent from a relay-able person, dropping...");
return;
}
} else if (rawEvent.sender !== puppetData.puppetMxid) {
log.verbose("Presence wasn't sent from the correct puppet, dropping...");
return;
}
const asUser = await this.getSendingUser(puppetData, "", rawEvent.sender);
const presence: IPresenceEvent = {
currentlyActive: rawEvent.content.currently_active,
lastActiveAgo: rawEvent.content.last_active_ago,
presence: rawEvent.content.presence,
statusMsg: rawEvent.content.status_msg,
};
log.verbose("Emitting presence event...");
this.bridge.emit("presence", puppetData.puppetId, presence, asUser, rawEvent);
}
// tslint:disable-next-line no-any
private async handleTyping(roomId: string, rawEvent: any) {
let first = true;
const pastTypingSet = this.typingCache.get(roomId) || new Set<string>();
const newTypingSet = new Set<string>(rawEvent.content.user_ids);
const changeUserIds = new Map<string, boolean>();
// first determine all the typing stops
for (const pastUserId of pastTypingSet) {
if (!newTypingSet.has(pastUserId)) {
changeUserIds.set(pastUserId, false);
}
}
// now determine all typing starts
for (const newUserId of newTypingSet) {
if (!pastTypingSet.has(newUserId)) {
changeUserIds.set(newUserId, true);
}
}
this.typingCache.set(roomId, newTypingSet);
for (const [userId, typing] of changeUserIds) {
if (this.bridge.AS.isNamespacedUser(userId)) {
continue; // we don't handle our own namespace
}
if (first) {
log.info(`Got typing event in room ${roomId}`);
first = false;
}
const room = await this.getRoomParts(roomId, userId);
if (!room) {
log.verbose("Room not found, ignoring...");
continue;
}
const puppetData = await this.bridge.provisioner.get(room.puppetId);
if (!puppetData) {
log.error("Puppet not found. Something is REALLY wrong!!!");
continue;
}
if (puppetData.type === "relay") {
if (!this.bridge.protocol.features.advancedRelay) {
log.verbose("Simple relays can't have foreign typing, dropping...");
continue;
}
if (!this.bridge.provisioner.canRelay(userId)) {
log.verbose("Typing wasn't sent from a relay-able person, dropping...");
continue;
}
} else if (userId !== puppetData.puppetMxid) {
log.verbose("Typing wasn't sent from the correct puppet, dropping...");
continue;
}
const asUser = await this.getSendingUser(puppetData, roomId, userId);
log.verbose("Emitting typing event...");
this.bridge.emit("typing", room, typing, asUser, rawEvent);
}
}
// tslint:disable-next-line no-any
private async handleReceipt(roomId: string, rawEvent: any) {
let first = true;
// tslint:disable-next-line no-any
for (const [eventId, allContents] of Object.entries<any>(rawEvent.content)) {
if (allContents["m.read"]) {
// we have read receipts
// tslint:disable-next-line no-any
for (const [userId, content] of Object.entries<any>(allContents["m.read"])) {
if (this.bridge.AS.isNamespacedUser(userId)) {
continue; // we don't handle our own namespace
}
if (first) {
log.info(`Got receipt event in room ${roomId}`);
first = false;
}
const room = await this.getRoomParts(roomId, userId);
if (!room) {
log.verbose("Room not found, dropping...");
continue;
}
const event = (await this.bridge.eventSync.getRemote(room, eventId))[0];
if (!event) {
log.verbose("Event not found, dropping...");
continue;
}
const puppetData = await this.bridge.provisioner.get(room.puppetId);
if (!puppetData) {
log.error("Puppet not found. Something is REALLY wrong!!!!");
continue;
}
if (puppetData.type === "relay") {
if (!this.bridge.protocol.features.advancedRelay) {
log.verbose("Simple relays can't have foreign receipts, dropping...");
continue;
}
if (!this.bridge.provisioner.canRelay(userId)) {
log.verbose("Receipt wasn't sent from a relay-able person, dropping...");
continue;
}
} else if (userId !== puppetData.puppetMxid) {
log.verbose("Receipt wasn't sent from the correct puppet, dropping...");
continue;
}
const asUser = await this.getSendingUser(puppetData, roomId, userId);
log.debug("Emitting read event...");
this.bridge.emit("read", room, event, content, asUser, rawEvent);
}
}
}
}
private getRoomDisplaynameCache(roomId: string): { [userId: string]: MembershipEventContent } {

@@ -681,3 +858,3 @@ if (!(roomId in this.memberInfoCache)) {

}
const membership = await this.getRoomMemberInfo(roomId, userId);
const membership = roomId ? await this.getRoomMemberInfo(roomId, userId) : null;
let user: IRemoteUser | null = null;

@@ -684,0 +861,0 @@ try {

@@ -17,2 +17,3 @@ /*

import { PresenceConfig } from "./config";
import { MatrixPresence } from "./interfaces";

@@ -25,4 +26,2 @@ // tslint:disable no-magic-numbers

export type MatrixPresence = "offline" | "online" | "unavailable";
interface IMatrixPresenceInfo {

@@ -29,0 +28,0 @@ mxid: string;

@@ -200,2 +200,6 @@ /*

this.bridge.emit("puppetNew", puppetId, data);
const WAIT_CONNECT_TIMEOUT = 10000;
setTimeout(async () => {
await this.adjustMuteListRooms(puppetId, puppetMxid);
}, WAIT_CONNECT_TIMEOUT);
return puppetId;

@@ -228,2 +232,3 @@ }

await this.puppetStore.delete(puppetId);
await this.adjustMuteEverywhere(puppetMxid);
this.bridge.emit("puppetDelete", puppetId);

@@ -370,2 +375,82 @@ }

public async adjustMute(userId: string, room: string) {
const MUTED_POWER_LEVEL = -1;
const UNMUTED_POWER_LEVEL = 0;
log.verbose(`Adjusting Mute for ${userId} in ${room}`);
let currentLevel = UNMUTED_POWER_LEVEL;
const client = await this.bridge.roomSync.getRoomOp(room);
if (!client) {
throw new Error("Failed to get operator of " + room);
}
if (!await client.userHasPowerLevelFor(userId, room, "m.room.message", false)) {
currentLevel = MUTED_POWER_LEVEL;
}
let targetLevel = UNMUTED_POWER_LEVEL;
const roomParts = await this.bridge.roomSync.resolve(room);
if (!roomParts) {
throw new Error("Failed to resolve room " + room);
}
const data = await this.bridge.roomStore.getByMxid(room);
if (!data) {
throw new Error("Failed to get data for " + room);
return;
}
if (!data || data.isDirect) {
log.verbose(`No muting in direct rooms`);
} else if (!await this.bridge.namespaceHandler.canSeeRoom(roomParts, userId)) {
targetLevel = MUTED_POWER_LEVEL;
}
if (currentLevel !== targetLevel) {
log.verbose(`Adjusting power level of ${userId} in ${room} to ${targetLevel}`);
await client.setUserPowerLevel(userId, room, targetLevel);
}
}
public async adjustMuteIfInRoom(userId: string, room: string) {
const client = await this.bridge.roomSync.getRoomOp(room);
if (!client) {
throw new Error("Failed to get operator of " + room);
}
try {
const members = await client.getJoinedRoomMembers(room);
if (!members.includes(userId)) {
return;
}
} catch (err) {
log.error("Error getting room members", err.error || err.body || err);
}
await this.adjustMute(userId, room);
}
public async adjustMuteListRooms(puppetId: number, userId: string) {
if (!this.bridge.hooks.listRooms) {
return;
}
const rooms = await this.bridge.hooks.listRooms(puppetId);
for (const r of rooms) {
if (!r.id!) {
continue;
}
const roomInfo = await this.bridge.roomSync.maybeGet({
puppetId,
roomId: r.id!,
});
if (!roomInfo) {
continue;
}
log.verbose(`roommxid: ${roomInfo.mxid}`);
await this.adjustMuteIfInRoom(userId, roomInfo.mxid);
}
}
public async adjustMuteEverywhere(userId: string) {
const entries = await this.bridge.roomStore.getAll();
for (const entry of entries) {
await this.adjustMuteIfInRoom(userId, entry.mxid);
}
}
public async invite(userId: string, ident: RemoteRoomResolvable): Promise<boolean> {

@@ -376,5 +461,9 @@ const roomParts = await this.bridge.roomSync.resolve(ident, userId);

}
const room = await this.bridge.roomSync.maybeGet(roomParts);
let room = await this.bridge.roomSync.maybeGet(roomParts);
if (!room) {
return false;
await this.bridge.bridgeRoom(roomParts);
room = await this.bridge.roomSync.maybeGet(roomParts);
if (!room) {
return false;
}
}

@@ -381,0 +470,0 @@ if (await this.bridge.namespaceHandler.canSeeRoom(room, userId)) {

@@ -46,3 +46,3 @@ /*

import { ProvisioningAPI } from "./provisioningapi";
import { PresenceHandler, MatrixPresence } from "./presencehandler";
import { PresenceHandler } from "./presencehandler";
import { TypingHandler } from "./typinghandler";

@@ -58,3 +58,3 @@ import { ReactionHandler } from "./reactionhandler";

ListRoomsHook, ListGroupsHook, IRemoteUser, IRemoteRoom, IRemoteGroup, IPuppetData, GetUserIdsInRoomHook,
UserExistsHook, RoomExistsHook, GroupExistsHook, ResolveRoomIdHook, IEventInfo,
UserExistsHook, RoomExistsHook, GroupExistsHook, ResolveRoomIdHook, IEventInfo, MatrixPresence,
} from "./interfaces";

@@ -268,6 +268,6 @@

const reg: IAppserviceRegistration = {
as_token: uuid(),
hs_token: uuid(),
id: opts.id,
namespaces: {
"as_token": uuid(),
"hs_token": uuid(),
"id": opts.id,
"namespaces": {
users: [{

@@ -283,6 +283,7 @@ exclusive: true,

},
protocols: [ ],
rate_limited: false,
sender_localpart: opts.botUser,
url: opts.url,
"protocols": [ ],
"rate_limited": false,
"sender_localpart": opts.botUser,
"url": opts.url,
"de.sorunome.msc2409.push_ephemeral": true,
};

@@ -354,6 +355,9 @@ fs.writeFileSync(this.registrationPath, yaml.safeDump(reg));

try {
if (displayname) {
const currProfile = await this.appservice.botIntent.underlyingClient.getUserProfile(
this.appservice.botIntent.userId,
);
if (displayname && displayname !== currProfile.displayname) {
await this.appservice.botIntent.underlyingClient.setDisplayName(displayname);
}
if (this.config.bridge.avatarUrl) {
if (this.config.bridge.avatarUrl && this.config.bridge.avatarUrl !== currProfile.avatar_url) {
await this.appservice.botIntent.underlyingClient.setAvatarUrl(this.config.bridge.avatarUrl);

@@ -360,0 +364,0 @@ }

@@ -18,4 +18,3 @@ /*

import { TimedCache } from "./structures/timedcache";
import { IRemoteUser, IReceiveParams, IMessageEvent } from "./interfaces";
import { MatrixPresence } from "./presencehandler";
import { IRemoteUser, IReceiveParams, IMessageEvent, MatrixPresence } from "./interfaces";
import {

@@ -26,2 +25,4 @@ TextualMessageEventContent, FileMessageEventContent, FileWithThumbnailInfo, MatrixClient, DimensionalFileInfo,

import * as escapeHtml from "escape-html";
import { encode as blurhashEncode } from "blurhash";
import * as Canvas from "canvas";

@@ -400,2 +401,44 @@ const log = new Log("RemoteEventHandler");

}
try {
const orientation = await Util.getExifOrientation(buffer);
const FIRST_EXIF_ROTATED = 5;
if (orientation > FIRST_EXIF_ROTATED) {
// flip width and height
const tmp = i.w;
i.w = i.h;
i.h = tmp;
}
} catch (err) {
log.debug("Error fetching exif orientation for image", err);
}
const BLURHASH_CHUNKS = 4;
const image = await new Promise<Canvas.Image>((resolve, reject) => {
const img = new Canvas.Image();
img.onload = () => resolve(img);
img.onerror = (...args) => reject(args);
img.src = "data:image/png;base64," + buffer.toString("base64");
});
let drawWidth = image.width;
let drawHeight = image.height;
const drawMax = 50;
if (drawWidth > drawMax || drawHeight > drawMax) {
if (drawWidth > drawHeight) {
drawHeight = Math.round(drawMax * (drawHeight / drawWidth));
drawWidth = drawMax;
} else {
drawWidth = Math.round(drawMax * (drawWidth / drawHeight));
drawHeight = drawMax;
}
}
const canvas = Canvas.createCanvas(drawWidth, drawHeight);
const context = canvas.getContext("2d");
if (context) {
context.drawImage(image, 0, 0, drawWidth, drawHeight);
const blurhashImageData = context.getImageData(0, 0, drawWidth, drawHeight);
// tslint:disable-next-line no-any
(i as any)["xyz.amorgan.blurhash"] = blurhashEncode(
blurhashImageData.data, drawWidth, drawHeight, BLURHASH_CHUNKS, BLURHASH_CHUNKS,
);
}
} catch (err) {

@@ -402,0 +445,0 @@ log.debug("Error adding information for image", err);

@@ -113,2 +113,3 @@ /*

}
length--; // else we gobble the ] twice
return {

@@ -157,2 +158,5 @@ if: resStrs[SEARCHING_IF],

if (res === result) {
if (!res) {
return "true";
}
return res;

@@ -159,0 +163,0 @@ }

@@ -25,5 +25,3 @@ /*

import { spawn } from "child_process";
import got, { Response } from "got";
// This import is weird and needs to stay weird as it isn't exported in the index file of got
import { OptionsOfDefaultResponseBody } from "got/dist/source/create";
import got, { Response, OptionsOfBufferResponseBody } from "got";

@@ -41,3 +39,6 @@ const log = new Log("Util");

export class Util {
public static async DownloadFile(url: string, options: OptionsOfDefaultResponseBody = {}): Promise<Buffer> {
public static async DownloadFile(
url: string,
options: OptionsOfBufferResponseBody = {responseType: "buffer"},
): Promise<Buffer> {
if (!options.method) {

@@ -270,2 +271,32 @@ options.method = "GET";

}
public static async getExifOrientation(buffer: Buffer): Promise<number> {
return new Promise<number>((resolve, reject) => {
const cmd = spawn("identify", ["-format", "'%[EXIF:Orientation]'", "-"]);
const TIMEOUT = 5000;
const timeout = setTimeout(() => {
cmd.kill();
}, TIMEOUT);
let databuf = "";
cmd.stdout.on("data", (data: string) => {
databuf += data;
});
cmd.stdout.on("error", (error) => {}); // disregard
cmd.on("error", (error) => {
cmd.kill();
clearTimeout(timeout);
reject(error);
});
cmd.on("close", (code: number) => {
clearTimeout(timeout);
try {
resolve(Number(databuf.replace(/.*(\d+).*/, "$1")));
} catch (err) {
reject(err);
}
});
cmd.stdin.on("error", (error) => {}); // disregard
cmd.stdin.end(buffer);
});
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc