implement basic group chats

This commit is contained in:
Robin Appelman 2020-05-10 16:04:42 +02:00
commit 6dd7bf525e
4 changed files with 140 additions and 29 deletions

View file

@ -5,14 +5,16 @@ Matrix <-> Steam puppeting bridge based on [mx-puppet-bridge](https://github.com
## Status ## Status
- [x] login with steam guard support - [x] login with steam guard support
- [x] 1 <->1 messaging - [x] 1<->1 messaging
- [ ] group messaging - [x] group messaging
- [x] steam -> matrix typing notifications - [x] steam -> matrix typing notifications
- [x] online/offline status - [x] online/offline status
- [x] retrieve nickname and avatar from steam - [x] retrieve nickname and avatar from steam
- [x] listing of steam users - [x] listing of steam users
- [ ] listing of steam group chats - [ ] listing of steam group chats
- [x] bridging embedded images - [x] bridging embedded images in 1<->1 chats
- [x] receiving embedded images from steam in group chats
- [ ] sending embedded images to steam in group chats
## Linking ## Linking

View file

@ -89,10 +89,10 @@ async function run() {
puppet.on("message", steam.handleMatrixMessage.bind(steam)); puppet.on("message", steam.handleMatrixMessage.bind(steam));
puppet.on("image", steam.handleMatrixImage.bind(steam)); puppet.on("image", steam.handleMatrixImage.bind(steam));
puppet.setCreateUserHook(steam.createUser.bind(steam)); puppet.setCreateUserHook(steam.createUser.bind(steam));
// puppet.setGetUserIdsInRoomHook(steam.getUserIdsInRoom.bind(steam));
puppet.setListUsersHook(steam.listUsers.bind(steam)); puppet.setListUsersHook(steam.listUsers.bind(steam));
puppet.setGetDmRoomIdHook(steam.getDmRoomId.bind(steam)); puppet.setGetDmRoomIdHook(steam.getDmRoomId.bind(steam));
puppet.setCreateRoomHook(steam.createRoom.bind(steam)); puppet.setCreateRoomHook(steam.createRoom.bind(steam));
puppet.setCreateGroupHook(steam.createGroup.bind(steam));
puppet.setGetDescHook(async (puppetId: number, data: any): Promise<string> => { puppet.setGetDescHook(async (puppetId: number, data: any): Promise<string> => {
let s = "Steam"; let s = "Steam";
if (data.screenName) { if (data.screenName) {

View file

@ -11,23 +11,31 @@ export interface IIncomingFriendMessage {
ordinal: number, ordinal: number,
local_echo: boolean, local_echo: boolean,
low_priority: boolean low_priority: boolean
message_bbcode_parsed: BBCodeNode[] message_bbcode_parsed: BBCodeField[]
}
export type BBCodeField = BBCodeNode | string;
export function isBBCode(field: BBCodeField): field is BBCodeNode {
return field['tag'] !== undefined;
} }
export interface BBCodeNode { export interface BBCodeNode {
tag: string tag: string
attrs: { [attr: string]: string }, attrs: { [attr: string]: string },
content: BBCodeNode[] content: BBCodeField[]
} }
export interface IIncomingChatMessage { export interface IIncomingChatMessage {
chat_group_id: string, chat_group_id: string,
chat_id: string, chat_id: string,
chat_name: string,
steamid_sender: SteamId, steamid_sender: SteamId,
chat_entry_type: EChatEntryType, chat_entry_type?: EChatEntryType,
from_limited_account: boolean, from_limited_account?: boolean,
message: string, message: string,
message_no_bbcode: string, message_no_bbcode: string,
message_bbcode_parsed: BBCodeField[]
server_timestamp: Date, server_timestamp: Date,
ordinal: number, ordinal: number,
mentions: IChatMentions | null, mentions: IChatMentions | null,
@ -82,3 +90,12 @@ export interface AppInfo {
} }
} }
} }
export interface IGroupInfo {
chat_group_id: string,
default_chat_id: string,
chat_rooms: {
chat_id: string,
chat_name: string,
}[]
}

View file

@ -13,8 +13,9 @@ import * as SteamCommunity from "steamcommunity";
import * as SteamID from "steamid"; import * as SteamID from "steamid";
import {EPersonaState} from "./enum"; import {EPersonaState} from "./enum";
import {MatrixPresence} from "mx-puppet-bridge/lib/src/presencehandler"; import {MatrixPresence} from "mx-puppet-bridge/lib/src/presencehandler";
import {AppInfo, IIncomingFriendMessage, IPersona} from "./interfaces"; import {AppInfo, IGroupInfo, IIncomingChatMessage, IIncomingFriendMessage, IPersona, isBBCode} from "./interfaces";
import {IRetList} from "mx-puppet-bridge/src/interfaces"; import {IRetList} from "mx-puppet-bridge/src/interfaces";
import {IRemoteGroup} from "mx-puppet-bridge/lib/src";
const log = new Log("MatrixPuppet:Steam"); const log = new Log("MatrixPuppet:Steam");
@ -25,7 +26,13 @@ interface ISteamPuppet {
sentEventIds: string[]; sentEventIds: string[];
knownPersonas: Map<string, IPersona>, knownPersonas: Map<string, IPersona>,
knownApps: Map<string, AppInfo>, knownApps: Map<string, AppInfo>,
ourSendImages: string[] ourSendImages: string[],
knownChats: Map<string, {
chat_group_id: string,
chat_id: string,
chat_name: string
}>,
knownGroupNames: Map<string, string>,
} }
interface ISteamPuppets { interface ISteamPuppets {
@ -75,7 +82,7 @@ export class Steam {
} }
} }
public async getSendParams(puppetId: number, msg: IIncomingFriendMessage, fromSteamId?: SteamID): Promise<IReceiveParams> { public async getFriendMessageSendParams(puppetId: number, msg: IIncomingFriendMessage, fromSteamId?: SteamID): Promise<IReceiveParams> {
const p = this.puppets[puppetId]; const p = this.puppets[puppetId];
let persona = await this.getPersona(p, fromSteamId ? fromSteamId : msg.steamid_friend); let persona = await this.getPersona(p, fromSteamId ? fromSteamId : msg.steamid_friend);
@ -96,6 +103,37 @@ export class Steam {
} as IReceiveParams; } as IReceiveParams;
} }
public async getChatMessageSendParams(puppetId: number, msg: IIncomingChatMessage, fromSteamId?: SteamID): Promise<IReceiveParams> {
const p = this.puppets[puppetId];
let persona = await this.getPersona(p, fromSteamId ? fromSteamId : msg.steamid_sender);
return {
room: {
puppetId,
roomId: `chat_${msg.chat_group_id}_${msg.chat_id}`,
isDirect: false,
name: msg.chat_name,
},
user: {
puppetId,
userId: fromSteamId ? fromSteamId.toString() : msg.steamid_sender.toString(),
name: persona.player_name,
avatarUrl: persona.avatar_url_medium
},
eventId: `${msg.server_timestamp.toISOString()}::${msg.ordinal}`,
} as IReceiveParams;
}
public parseChatRoomId(roomId: string): [string, string] {
let matches = roomId.match(/chat_(\d+)_(\d+)/);
if (matches) {
return [matches[1], matches[2]];
} else {
throw new Error("invalid chatroom id");
}
}
public async newPuppet(puppetId: number, data: IPuppetParams) { public async newPuppet(puppetId: number, data: IPuppetParams) {
log.info(`Adding new Puppet: puppetId=${puppetId}`); log.info(`Adding new Puppet: puppetId=${puppetId}`);
if (this.puppets[puppetId]) { if (this.puppets[puppetId]) {
@ -113,6 +151,8 @@ export class Steam {
knownPersonas: new Map(), knownPersonas: new Map(),
knownApps: new Map(), knownApps: new Map(),
ourSendImages: [], ourSendImages: [],
knownChats: new Map(),
knownGroupNames: new Map(),
} as ISteamPuppet; } as ISteamPuppet;
try { try {
client.logOn({ client.logOn({
@ -189,6 +229,9 @@ export class Steam {
client.chat.on("friendTyping", (message: IIncomingFriendMessage) => { client.chat.on("friendTyping", (message: IIncomingFriendMessage) => {
this.handleFriendTyping(puppetId, message); this.handleFriendTyping(puppetId, message);
}); });
client.chat.on("chatMessage", (message) => {
this.handleChatMessage(puppetId, message);
});
client.on("error", (err) => { client.on("error", (err) => {
log.error(`Failed to start up puppet ${puppetId}`, err); log.error(`Failed to start up puppet ${puppetId}`, err);
@ -227,23 +270,51 @@ export class Steam {
public async handleFriendMessage(puppetId: number, message: IIncomingFriendMessage, fromSteamId?: SteamID) { public async handleFriendMessage(puppetId: number, message: IIncomingFriendMessage, fromSteamId?: SteamID) {
const p = this.puppets[puppetId]; const p = this.puppets[puppetId];
log.verbose("Got message from steam to pass on"); log.verbose("Got friend message from steam to pass on");
let sendParams = await this.getSendParams(puppetId, message, fromSteamId); let sendParams = await this.getFriendMessageSendParams(puppetId, message, fromSteamId);
await this.sendMessage(p, sendParams, message);
}
public async handleChatMessage(puppetId: number, message: IIncomingChatMessage, fromSteamId?: SteamID) {
const p = this.puppets[puppetId];
log.verbose("Got chat message from steam to pass on");
if (!p.knownChats.has(message.chat_id)) {
p.knownChats.set(message.chat_id, {
chat_id: message.chat_id,
chat_group_id: message.chat_group_id,
chat_name: message.chat_name
});
}
if (!p.knownGroupNames.has(message.chat_group_id)) {
let parts = message.chat_name.split('|');
p.knownGroupNames.set(message.chat_group_id, parts[0].trim());
}
let sendParams = await this.getChatMessageSendParams(puppetId, message, fromSteamId);
await this.sendMessage(p, sendParams, message);
}
public async sendMessage(puppet: ISteamPuppet, sendParams: IReceiveParams, message: IIncomingFriendMessage | IIncomingChatMessage) {
// message is only an embedded image // message is only an embedded image
if ( if (
message.message_bbcode_parsed.length === 1 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_bbcode_parsed[0].tag === 'img'
&& message.message_no_bbcode === message.message_bbcode_parsed[0].attrs['src'] && message.message_no_bbcode === message.message_bbcode_parsed[0].attrs['src']
) { ) {
const url = message.message_bbcode_parsed[0].attrs['src']; const url = message.message_bbcode_parsed[0].attrs['src'];
let i = p.ourSendImages.indexOf(url); let i = puppet.ourSendImages.indexOf(url);
if (i === -1) { if (i === -1) {
await this.bridge.sendImage(sendParams, url); await this.bridge.sendImage(sendParams, url);
} else { } else {
// image came from us, dont send // image came from us, dont send
p.ourSendImages.splice(i); puppet.ourSendImages.splice(i);
} }
} else { } else {
await this.bridge.sendMessage(sendParams, { await this.bridge.sendMessage(sendParams, {
@ -279,7 +350,13 @@ export class Steam {
await this.bridge.eventSync.insert(room, eventId, id); await this.bridge.eventSync.insert(room, eventId, id);
p.sentEventIds.push(id); p.sentEventIds.push(id);
} else { } else {
await this.bridge.sendStatusMessage(room.puppetId, `Sending group messages is currently not supported`); let [groupId, chatId] = this.parseChatRoomId(room.roomId);
const sendMessage = await p.client.chat.sendChatMessage(groupId, chatId, msg);
let id = `${sendMessage.server_timestamp.toISOString()}::${sendMessage.ordinal}`;
await this.bridge.eventSync.insert(room, eventId, id);
p.sentEventIds.push(id);
} }
} }
@ -369,21 +446,36 @@ export class Steam {
puppetId: room.puppetId, puppetId: room.puppetId,
roomId: room.roomId, roomId: room.roomId,
isDirect: true, isDirect: true,
topic: persona.player_name name: persona.player_name
}; };
} else { } else {
await this.bridge.sendStatusMessage(room.puppetId, `Creating group room chats is currently not supported`); let [groupId, chatId] = this.parseChatRoomId(room.roomId);
return null; let chatRoom = p.knownChats.get(chatId);
if (chatRoom) {
return {
puppetId: room.puppetId,
roomId: `chat_${chatRoom.chat_group_id}_${chatRoom.chat_id}`,
isDirect: false,
groupId: chatRoom.chat_group_id,
name: chatRoom.chat_name,
};
} }
} }
// public async getUserIdsInRoom(room: IRemoteRoom): Promise<Set<string> | null> { await this.bridge.sendStatusMessage(room.puppetId, `Invalid room id or unknown chat: ${room.roomId}`);
// const p = this.puppets[room.puppetId]; return null;
// const client: TalkClient = p.client; }
// const participants = await client.getChat(room.roomId).get_participants();
// for (const participant of participants) { public async createGroup(room: IRemoteGroup): Promise<IRemoteGroup | null> {
// p.knownUserNames[participant.userId] = participant.displayName; const p = this.puppets[room.puppetId];
// } if (!p) {
// return new Set(participants.map((participant) => participant.userId)); return null;
// } }
return {
puppetId: room.puppetId,
groupId: room.groupId,
shortDescription: p.knownGroupNames.get(room.groupId),
}
}
} }