support bridging emotes

This commit is contained in:
Robin Appelman 2020-12-25 21:53:06 +01:00
commit 09ea312a47
7 changed files with 341 additions and 149 deletions

View file

@ -16,10 +16,6 @@ export interface IIncomingFriendMessage {
export type BBCodeField = BBCodeNode | string;
export function isBBCode(field: BBCodeField): field is BBCodeNode {
return field['tag'] !== undefined;
}
export interface BBCodeNode {
tag: string
attrs: { [attr: string]: string },

View file

@ -28,7 +28,7 @@ export interface LoginToken {
steamID: SteamID
}
// export async function login(details: LoginDetails, steamGuard?: () => Promise<string>, twoFactor?: () => Promise<string>, captcha?: (url: string) => Promise<string>): Promise<LoginToken> {
// export async function login(details: LoginDetails, steamGuard?: () => Promise<string>, twoFactor?: () => Promise<string>, captcha?: (urlOrBuffer: string) => Promise<string>): Promise<LoginToken> {
// let community = new SteamCommunity();
// while (true) {
// try {

123
src/message.ts Normal file
View file

@ -0,0 +1,123 @@
import {BBCodeField, BBCodeNode, IIncomingChatMessage, IIncomingFriendMessage} from "./interfaces";
import {Steam} from "./steam";
import UPNG from "@pdf-lib/upng";
const GIFEncoder = require("gif-encoder");
import {WritableStreamBuffer} from 'stream-buffers';
import {Util} from "mx-puppet-bridge";
function isBBCode(field: BBCodeField): field is BBCodeNode {
return field['tag'] !== undefined;
}
async function apngToGif(sourceUrl: string): Promise<Buffer> {
const input = await Util.DownloadFile(sourceUrl);
const pngImage = UPNG.decode(input);
let frameData = UPNG.toRGBA8(pngImage);
const encoder = new GIFEncoder(pngImage.width, pngImage.height);
let output = new WritableStreamBuffer();
encoder.pipe(output);
encoder.setRepeat(0);
encoder.setTransparent(0xFF00FF);
encoder.writeHeader();
for (let i = 0; i < pngImage.frames.length; i++) {
let frame = pngImage.frames[i];
let data = new Uint8Array(frameData[i]);
// set transparent pixels to 0xFF00FF
for (let y = 0; y < data.length; y += 4) {
if (data[y + 3] < 200) {
data[y] = 0xFF;
data[y + 1] = 0x00;
data[y + 2] = 0xFF;
}
}
encoder.setDelay(frame.delay);
encoder.addFrame(data);
}
return output.getContents();
}
async function formatBBCode(steam: Steam, puppetId: number, node: BBCodeNode): Promise<ImageMessage | TextMessage> {
if (node.tag === 'img') {
return {
kind: "image",
urlOrBuffer: node.attrs['src']
};
} else if (node.tag === 'emoticon') {
let emote = node.content[0] as string;
const mxc = await steam.getEmojiMxc(
puppetId, 'emoticonlarge', emote,
);
return {
kind: "text",
body: `:${emote}:`,
formattedBody: `<img alt=":${emote}:" title=":${emote}:" height="32" src="${mxc}" data-mx-emoticon />`
};
} else if (node.tag === 'sticker') {
let sticker = node.attrs['type'];
let gif = await apngToGif(`https://community.cloudflare.steamstatic.com/economy/sticker/${sticker}`);
return {
kind: "image",
urlOrBuffer: gif
};
} else {
return {kind: "text", body: `[${node.tag}]`};
}
}
export async function exportMessageForSending(steam: Steam, puppetId: number, message: IIncomingFriendMessage | IIncomingChatMessage): Promise<(TextMessage | ImageMessage)[]> {
if (message.message_bbcode_parsed) {
let parts = await Promise.all(message.message_bbcode_parsed.map(node => {
if (isBBCode(node)) {
return formatBBCode(steam, puppetId, node);
} else {
return {kind: "text", body: node} as TextMessage;
}
}));
return parts.reduce((merged, part) => {
if (merged.length === 0) {
merged.push(part);
} else {
let last = merged[merged.length - 1];
// merge adjacent text nodes
if (last.kind === "text" && part.kind === "text") {
if (!last.formattedBody) {
last.formattedBody = last.body;
}
if (!part.formattedBody) {
part.formattedBody = part.body;
}
last.body += " " + part.body;
last.formattedBody += " " + part.formattedBody;
} else {
merged.push(part);
}
}
return merged;
}, [] as (TextMessage | ImageMessage)[]);
} else {
return [{kind: "text", body: message.message_no_bbcode}];
}
}
export interface TextMessage {
kind: "text";
body: string;
formattedBody?: string;
}
export interface ImageMessage {
kind: "image";
urlOrBuffer: string | Buffer
}

View file

@ -20,10 +20,10 @@ import {
IGroupDetails,
IIncomingChatMessage,
IIncomingFriendMessage,
IPersona,
isBBCode
IPersona
} from "./interfaces";
import {debounce} from 'ts-debounce';
import {exportMessageForSending} from "./message";
const log = new Log("MatrixPuppet:Steam");
@ -284,7 +284,7 @@ export class Steam {
let sendParams = await this.getFriendMessageSendParams(puppetId, message, fromSteamId);
await this.sendMessage(p, sendParams, message);
await this.sendMessage(p, puppetId, sendParams, message);
}
public async handleChatMessage(puppetId: number, message: IIncomingChatMessage, fromSteamId?: SteamID) {
@ -293,30 +293,28 @@ export class Steam {
let sendParams = await this.getChatMessageSendParams(puppetId, message, fromSteamId);
await this.sendMessage(p, sendParams, message);
await this.sendMessage(p, puppetId, sendParams, message);
}
public async sendMessage(puppet: ISteamPuppet, sendParams: IReceiveParams, message: IIncomingFriendMessage | IIncomingChatMessage) {
// message is only an embedded image
if (
message.message_bbcode_parsed
&& message.message_bbcode_parsed.length === 1
&& isBBCode(message.message_bbcode_parsed[0])
&& message.message_bbcode_parsed[0].tag === 'img'
&& message.message_no_bbcode === message.message_bbcode_parsed[0].attrs['src']
) {
const url = message.message_bbcode_parsed[0].attrs['src'];
let i = puppet.ourSendImages.indexOf(url);
if (i === -1) {
await this.bridge.sendImage(sendParams, url);
} else {
// image came from us, dont send
puppet.ourSendImages.splice(i);
public async sendMessage(puppet: ISteamPuppet, puppetId: number, sendParams: IReceiveParams, incoming: IIncomingFriendMessage | IIncomingChatMessage) {
const parts = await exportMessageForSending(this, puppetId, incoming);
for (let part of parts) {
if (part.kind === "image") {
const imageUrl = part.urlOrBuffer;
let i = (typeof imageUrl === "string") ? puppet.ourSendImages.indexOf(imageUrl) : -1;
if (i === -1) {
await this.bridge.sendImage(sendParams, imageUrl);
} else {
// image came from us, dont send
puppet.ourSendImages.splice(i);
}
} else if (part.kind === "text") {
await this.bridge.sendMessage(sendParams, {
body: part.body,
formattedBody: part.formattedBody,
});
}
} else {
await this.bridge.sendMessage(sendParams, {
body: message.message_no_bbcode,
});
}
}
@ -382,7 +380,8 @@ export class Steam {
let steamId = this.getRoomSteamId(room);
if (steamId) {
const bufferPromise = Util.DownloadFile(data.url);;
const bufferPromise = Util.DownloadFile(data.url);
;
await new Promise((resolve, _reject) => {
p.client.webLogOn();
@ -560,4 +559,26 @@ export class Steam {
p.client.setPersona(EPersonaState.Away);
}
}
public async getEmojiMxc(puppetId: number, type: 'sticker' | 'emoticonlarge', name: string): Promise<string | null> {
const id = `${type}/${name}`;
const emoji = await this.bridge.emoteSync.get({
puppetId,
emoteId: id,
});
if (emoji && emoji.avatarMxc) {
return emoji.avatarMxc;
}
const {emote} = await this.bridge.emoteSync.set({
puppetId,
emoteId: id,
avatarUrl: `https://community.cloudflare.steamstatic.com/economy/${type}/${encodeURIComponent(name)}`,
name,
data: {
type,
name,
},
});
return emote.avatarMxc || null;
}
}