seems to work somewhat

This commit is contained in:
Robin Appelman 2020-05-05 18:40:44 +02:00
commit 7efed83afd
12 changed files with 7432 additions and 0 deletions

3423
src/@types/steam-user/enum.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

22
src/@types/steam-user/index.d.ts vendored Normal file
View file

@ -0,0 +1,22 @@
// declare module 'steam-user' {
// export * from "steam-user-enums";
//
// export interface ILoginParams {
// accountName: string;
// password: string;
// }
//
// export class Chat {
//
// }
//
// export default class SteamUser {
// constructor();
//
// public logOn(params: any): void;
//
// public on(hook: string, callback: any): void;
// }
// }
//
//

54
src/@types/steam-user/interfaces.d.ts vendored Normal file
View file

@ -0,0 +1,54 @@
declare module 'steam-user-interfaces' {
import {EChatEntryType, EChatRoomServerMessage} from "steam-user-enums";
import SteamId from "steamid";
export interface IIncomingFriendMessage {
steamid_friend: SteamId,
chat_entry_type: EChatEntryType,
from_limited_account: boolean,
message: string,
message_no_bbcode: string,
server_timestamp: Date,
ordinal: number,
local_echo: boolean,
low_priority: boolean
}
export interface IIncomingChatMessage {
chat_group_id: string,
chat_id: string,
steamid_sender: SteamId,
chat_entry_type: EChatEntryType,
from_limited_account: boolean,
message: string,
message_no_bbcode: string,
server_timestamp: Date,
ordinal: number,
mentions: IChatMentions | null,
}
export interface IChatMentions {
mention_all: boolean,
mention_here,
mention_steamids: SteamId[]
}
export interface IServerMessage {
message: EChatRoomServerMessage,
string_param?: string,
steamid_param?: SteamId
}
// incomplete
export interface IPersona {
player_name: string,
avatar_hash: Buffer,
last_logoff: Date,
last_logon: Date,
last_seen_online: Date,
game_name: string,
avatar_url_icon: string,
avatar_url_medium: string,
avatar_url_full: string
}
}

16
src/config.ts Normal file
View file

@ -0,0 +1,16 @@
export class NextcloudConfigWrap {
public nextcloud: NextcloudConfig = new NextcloudConfig();
public applyConfig(newConfig: { [key: string]: any }, configLayer: { [key: string]: any } = this) {
Object.keys(newConfig).forEach((key) => {
if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) {
this.applyConfig(newConfig[key], configLayer[key]);
} else {
configLayer[key] = newConfig[key];
}
});
}
}
class NextcloudConfig {
}

170
src/index.ts Normal file
View file

@ -0,0 +1,170 @@
import {
PuppetBridge,
IProtocolInformation,
IPuppetBridgeRegOpts,
Log,
IRetData,
} from "mx-puppet-bridge";
import * as commandLineArgs from "command-line-args";
import * as commandLineUsage from "command-line-usage";
import * as escapeHtml from "escape-html";
import {Steam} from "./steam";
import {NextcloudConfigWrap} from "./config";
import * as fs from "fs";
import * as yaml from "js-yaml";
import {GetDataFromStrHook, IPuppetData} from "mx-puppet-bridge/lib/src";
import * as SteamCommunity from 'steamcommunity';
import {LoginDetails, LoginToken} from "./login";
import * as SteamID from "steamid";
import * as SteamUser from "steam-user";
const log = new Log("NextcloudPuppet:index");
const commandOptions = [
{name: "register", alias: "r", type: Boolean},
{name: "registration-file", alias: "f", type: String},
{name: "config", alias: "c", type: String},
{name: "help", alias: "h", type: Boolean},
];
const options = Object.assign({
"register": false,
"registration-file": "steam-registration.yaml",
"config": "config.yaml",
"help": false,
}, commandLineArgs(commandOptions));
if (options.help) {
// tslint:disable-next-line:no-console
console.log(commandLineUsage([
{
header: "Matrix Stream Puppet Bridge",
content: "A matrix puppet bridge for steam",
},
{
header: "Options",
optionList: commandOptions,
},
]));
process.exit(0);
}
const protocol = {
features: {typingTimeout : 1 * 1000},
id: "steam",
displayname: "Steam",
externalUrl: "https://steamcommunity.com/",
} as IProtocolInformation;
const puppet = new PuppetBridge(options["registration-file"], options.config, protocol);
if (options.register) {
// okay, all we have to do is generate a registration file
puppet.readConfig(false);
try {
puppet.generateRegistration({
prefix: "_steampuppet_",
id: "steam-puppet",
url: `http://${puppet.Config.bridge.bindAddress}:${puppet.Config.bridge.port}`,
} as IPuppetBridgeRegOpts);
} catch (err) {
// tslint:disable-next-line:no-console
console.log("Couldn't generate registration file:", err);
}
process.exit(0);
}
let config: NextcloudConfigWrap = new NextcloudConfigWrap();
function readConfig() {
config = new NextcloudConfigWrap();
config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config)));
}
export function Config(): NextcloudConfigWrap {
return config;
}
async function run() {
await puppet.init();
// readConfig();
const steam = new Steam(puppet);
puppet.on("puppetNew", steam.newPuppet.bind(steam));
puppet.on("puppetDelete", steam.deletePuppet.bind(steam));
puppet.on("message", steam.handleMatrixMessage.bind(steam));
puppet.on("image", steam.handleMatrixImage.bind(steam));
puppet.on("video", steam.handleMatrixVideo.bind(steam));
puppet.setCreateUserHook(steam.createUser.bind(steam));
// puppet.setGetUserIdsInRoomHook(steam.getUserIdsInRoom.bind(steam));
puppet.setGetDescHook(async (puppetId: number, data: any): Promise<string> => {
let s = "Steam";
if (data.screenName) {
s += ` as ${data.screenName}`;
}
if (data.name) {
s += ` (${data.name})`;
}
return s;
});
puppet.setGetDataFromStrHook(async (str: string): Promise<IRetData> => {
if (!str) {
return {
success: false,
error: `Usage: link <username> <password>`
};
}
let [username, password] = str.split(" ", 2);
const client = new SteamUser();
let details: LoginDetails = {
accountName: username,
password,
rememberPassword: true,
};
return new Promise(resolve => {
let successResolve = resolve;
client.on("loginKey", function(loginKey) {
successResolve({
success: true,
data: {
accountName: username,
loginKey
}
});
});
client.on("steamGuard", function(domain, cb) {
resolve({
success: false,
error: `Please provide steam guard code`,
fn: async (code) => {
cb(code);
return new Promise(resolve => {
successResolve = resolve;
})
}
});
});
client.on("error", (err) => {
resolve({
success: false,
error: err.message
});
});
client.logOn(details);
});
});
await puppet.start();
}
// tslint:disable-next-line:no-floating-promises
run(); // start the thing!

71
src/login.ts Normal file
View file

@ -0,0 +1,71 @@
import SteamID from "steamid";
const SteamCommunity = require('steamcommunity');
function tryLogin(community, details): Promise<void> {
return new Promise((res, rej) => community.login(details, function(err) {
if (err) {
rej(err);
} else {
res();
}
}));
}
export interface LoginDetails {
"accountName": string,
"password": string,
"steamguard"?: string,
"authCode"?: string,
"twoFactorCode"?: string,
"captcha"?: string,
disableMobile?: boolean,
rememberPassword?: boolean
}
export interface LoginToken {
accountName: string,
webLogonToken: string,
steamID: SteamID
}
// export async function login(details: LoginDetails, steamGuard?: () => Promise<string>, twoFactor?: () => Promise<string>, captcha?: (url: string) => Promise<string>): Promise<LoginToken> {
// let community = new SteamCommunity();
// while (true) {
// try {
// await tryLogin(details);
// return new Promise((res, rej) => community.getClientLogonToken((err, details) => {
// if (err) {
// rej(err);
// } else {
// res(details);
// }
// }));
// } catch (e) {
// switch (e.message) {
// case 'SteamGuard':
// if (steamGuard) {
// details.steamguard = await steamGuard();
// } else {
// throw new Error("No steamguard handler provided")
// }
// break;
// case 'SteamGuardMobile':
// if (twoFactor) {
// details.twoFactorCode = await twoFactor();
// } else {
// throw new Error("No twoFactor handler provided")
// }
// break;
// case 'CAPTCHA':
// if (captcha) {
// details.captcha = await captcha(e.captchaurl);
// } else {
// throw new Error("No twoFactor handler provided")
// }
// break;
// default:
// throw e
// }
// }
// }
// }

259
src/steam.ts Normal file
View file

@ -0,0 +1,259 @@
/// <reference path="@types/steam-user/enum.d.ts" />
/// <reference path="@types/steam-user/interfaces.d.ts" />
import {
PuppetBridge,
Log,
IReceiveParams,
IRemoteRoom,
IRemoteUser,
IMessageEvent,
IFileEvent,
Util,
IRetList,
} from "mx-puppet-bridge";
import * as SteamUser from "steam-user";
import * as SteamID from "steamid";
import {IIncomingFriendMessage, IPersona} from "steam-user-interfaces";
const log = new Log("MatrixPuppet:Steam");
interface ISteamPuppet {
client: SteamUser;
data: any;
sentEventIds: string[];
typingUsers: { [key: string]: any };
knownPersonas: Map<string, IPersona>,
}
interface ISteamPuppets {
[puppetId: number]: ISteamPuppet;
}
interface IPuppetParams {
accountName: string,
loginKey: string,
[key: string]: string;
}
export class Steam {
private puppets: ISteamPuppets = {};
constructor(
private bridge: PuppetBridge,
) {
}
async getPersona(p: ISteamPuppet, steamid: SteamID): Promise<IPersona> {
let steamIdString = steamid.toString();
let persona = p.knownPersonas.get(steamIdString)
if (persona) {
return persona;
} else if (p.client.users[steamIdString]) {
return p.client.users[steamIdString];
} else {
let {personas} = await p.client.getPersonas([steamid]);
let persona = personas[steamIdString];
p.knownPersonas.set(steamIdString, persona);
return persona;
}
}
public async getSendParams(puppetId: number, msg: IIncomingFriendMessage, fromSteamId?: SteamID): Promise<IReceiveParams> {
const p = this.puppets[puppetId];
let persona = await this.getPersona(p, fromSteamId ? fromSteamId : msg.steamid_friend);
return {
room: {
puppetId,
roomId: msg.steamid_friend.toString(),
isDirect: true,
},
user: {
puppetId,
userId: fromSteamId ? fromSteamId.toString() : msg.steamid_friend.toString(),
name: persona.player_name,
avatarUrl: persona.avatar_url_medium
},
eventId: `${msg.server_timestamp.toISOString()}::${msg.ordinal}`,
} as IReceiveParams;
}
public async newPuppet(puppetId: number, data: IPuppetParams) {
log.info(`Adding new Puppet: puppetId=${puppetId}`);
if (this.puppets[puppetId]) {
await this.deletePuppet(puppetId);
}
const client = new SteamUser();
this.puppets[puppetId] = {
client,
data,
sentEventIds: [],
typingUsers: {},
knownPersonas: new Map(),
} as ISteamPuppet;
try {
client.logOn({
accountName: data.accountName,
loginKey: data.loginKey
});
client.on("user", (steamId, persona) => {
this.puppets[puppetId].knownPersonas.set(steamId.toString(), persona);
});
client.on("loggedOn", async (details) => {
await this.bridge.setUserId(puppetId, client.steamID.toString());
await this.bridge.sendStatusMessage(puppetId, `connected as ${details.vanity_url}(${client.steamID.toString()})!`);
})
client.on("loginKey", (loginKey) => {
console.log("got new login key");
data.loginKey = loginKey;
this.bridge.setPuppetData(puppetId, data);
});
client.chat.on("friendMessage", (message) => {
this.handleFriendMessage(puppetId, message);
});
client.chat.on("friendMessageEcho", (message) => {
this.handleFriendMessage(puppetId, message, client.steamID);
});
client.chat.on("friendTyping", (message: IIncomingFriendMessage) => {
this.handleFriendTyping(puppetId, message);
})
client.on("error", (err) => {
log.error(`Failed to start up puppet ${puppetId}`, err);
this.bridge.sendStatusMessage(puppetId, `**disconnected!**: failed to connect. ${err}`);
});
} catch (err) {
log.error(`Failed to start up puppet ${puppetId}`, err);
await this.bridge.sendStatusMessage(puppetId, `**disconnected!**: failed to connect. ${err}`);
}
}
public async deletePuppet(puppetId: number) {
log.info(`Got signal to quit Puppet: puppetId=${puppetId}`);
const p = this.puppets[puppetId];
if (!p) {
return; // nothing to do
}
p.client.logOff();
delete this.bridge[puppetId];
}
public async handleFriendMessage(puppetId: number, message: IIncomingFriendMessage, fromSteamId?: SteamID) {
const p = this.puppets[puppetId];
log.verbose("Got message from steam to pass on");
const typingKey = message.steamid_friend.toString();
if (p.typingUsers[typingKey]) {
// user is typing, stop that
await this.bridge.setUserTyping(p.typingUsers[typingKey], false);
delete p.typingUsers[typingKey];
}
await this.bridge.sendMessage(await this.getSendParams(puppetId, message, fromSteamId), {
body: message.message,
});
}
public async handleFriendTyping(puppetId: number, message: IIncomingFriendMessage) {
const p = this.puppets[puppetId];
const typingKey = message.steamid_friend.toString();
p.typingUsers[typingKey] = {
room: {
puppetId,
roomId: message.steamid_friend.toString(),
},
user: {
puppetId,
userId: message.steamid_friend.toString(),
},
};
await this.bridge.setUserTyping(p.typingUsers[typingKey], true);
}
public async sendMessageToSteam(
p: ISteamPuppet,
room: IRemoteRoom,
eventId: string,
msg: string,
mediaId?: string,
) {
let id = "";
try {
let _roomSteamId = new SteamID(room.name as string);
if (_roomSteamId.isValid()) {
const sendMessage = await p.client.chat.sendFriendMessage(room.roomId, msg);
id = `${sendMessage.server_timestamp.toISOString()}::${sendMessage.ordinal}`;
} else {
await this.bridge.sendStatusMessage(room.puppetId, `Sending group messages is currently not supported`);
}
} catch (e) {
await this.bridge.sendStatusMessage(room.puppetId, `Sending group messages is currently not supported`);
}
await this.bridge.eventSync.insert(room, eventId, id);
p.sentEventIds.push(id);
}
public async handleMatrixMessage(room: IRemoteRoom, data: IMessageEvent, event: any) {
const p = this.puppets[room.puppetId];
if (!p) {
return;
}
log.verbose("Got message to send on");
// room.roomId, data.body
await this.sendMessageToSteam(p, room, data.eventId!, data.body);
}
public async handleMatrixImage(room: IRemoteRoom, data: IFileEvent, event: any) {
const p = this.puppets[room.puppetId];
if (!p) {
return;
}
log.verbose("Got image to send on");
}
public async handleMatrixVideo(room: IRemoteRoom, data: IFileEvent, event: any) {
const p = this.puppets[room.puppetId];
if (!p) {
return;
}
log.verbose("Got video to send on");
}
public async createUser(user: IRemoteUser): Promise<IRemoteUser | null> {
const p = this.puppets[user.puppetId];
if (!p) {
return null;
}
log.info(`Got request to create user ${user.userId}`);
return {
userId: user.userId,
puppetId: user.puppetId,
name: user.name,
};
}
// public async getUserIdsInRoom(room: IRemoteRoom): Promise<Set<string> | null> {
// const p = this.puppets[room.puppetId];
// const client: TalkClient = p.client;
// const participants = await client.getChat(room.roomId).get_participants();
// for (const participant of participants) {
// p.knownUserNames[participant.userId] = participant.displayName;
// }
// return new Set(participants.map((participant) => participant.userId));
// }
}