mx-puppet-bridge
Advanced tools
Comparing version 0.0.45 to 0.1.0-1
@@ -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 { |
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); | ||
}); | ||
} | ||
} |
877504
22360
16
+ Addedblurhash@^1.1.3
+ Addedcanvas@^2.6.1
+ Added@mapbox/node-pre-gyp@1.0.11(transitive)
+ Addedabbrev@1.1.1(transitive)
+ Addedagent-base@6.0.2(transitive)
+ Addedansi-regex@5.0.1(transitive)
+ Addedaproba@2.0.0(transitive)
+ Addedare-we-there-yet@2.0.0(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedblurhash@1.1.5(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedcanvas@2.11.2(transitive)
+ Addedchownr@2.0.0(transitive)
+ Addedcolor-support@1.1.3(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addeddebug@4.3.4(transitive)
+ Addeddetect-libc@2.0.3(transitive)
+ Addedemoji-regex@8.0.0(transitive)
+ Addedfs-minipass@2.1.0(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedgauge@3.0.2(transitive)
+ Addedglob@7.2.3(transitive)
+ Addedhttps-proxy-agent@5.0.1(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedis-fullwidth-code-point@3.0.0(transitive)
+ Addedmake-dir@3.1.0(transitive)
+ Addedminimatch@3.1.2(transitive)
+ Addedminipass@3.3.65.0.0(transitive)
+ Addedminizlib@2.1.2(transitive)
+ Addedms@2.1.2(transitive)
+ Addednan@2.19.0(transitive)
+ Addednode-fetch@2.7.0(transitive)
+ Addednopt@5.0.0(transitive)
+ Addednpmlog@5.0.1(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedrimraf@3.0.2(transitive)
+ Addedsemver@7.6.0(transitive)
+ Addedstring-width@4.2.3(transitive)
+ Addedstrip-ansi@6.0.1(transitive)
+ Addedtar@6.2.1(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)
- Removed@sindresorhus/is@2.1.1(transitive)
- Removedcacheable-lookup@2.0.1(transitive)
- Removeddecompress-response@5.0.0(transitive)
- Removedduplexer3@0.1.5(transitive)
- Removedgot@10.7.0(transitive)
- Removedp-event@4.2.0(transitive)
- Removedp-finally@1.0.0(transitive)
- Removedp-timeout@3.2.0(transitive)
- Removedto-readable-stream@2.1.0(transitive)
- Removedtype-fest@0.10.0(transitive)
Updatedgot@^11.6.0