This commit is contained in:
Robin Appelman 2020-05-08 19:36:04 +02:00
commit cc607167ec
12 changed files with 3625 additions and 4351 deletions

View file

@ -4,8 +4,12 @@ Matrix <-> Steam puppeting bridge based on [mx-puppet-bridge](https://github.com
## Status ## Status
The bridge is in early status, logging in and 1<->1 messages are mostly working. - [x] login with steam guard support
Group messages are not working, typing status works somewhat. - [x] 1 <->1 messaging
- [ ] group messaging
- [x] steam -> matrix typing notifications
- [x] online/offline status
- [x] retrieve nickname and avatar from steam
## Linking ## Linking
@ -15,4 +19,4 @@ Start a chat with @_steampuppet_bot:yourserver.com
link <username> <password> link <username> <password>
``` ```
If a steam guard (mobile or email) code is required you will be asked for the code. If a steam guard (mobile or email) code is required, you will be asked for the code.

765
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,15 +11,12 @@
}, },
"author": "Icewind", "author": "Icewind",
"dependencies": { "dependencies": {
"body-parser": "^1.19.0",
"command-line-args": "^5.1.1", "command-line-args": "^5.1.1",
"command-line-usage": "^5.0.5", "command-line-usage": "^5.0.5",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"mx-puppet-bridge": "0.0.40", "mx-puppet-bridge": "0.0.43",
"steam-user": "4.15.1", "steam-user": "4.15.1",
"steamcommunity": "3.41.3",
"steamid": "1.1.3", "steamid": "1.1.3",
"tslint": "^5.20.1",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"devDependencies": { "devDependencies": {

View file

@ -22,3 +22,10 @@ provisioning:
#sharedSecret: random string #sharedSecret: random string
# Path prefix for the provisioning API. /v1 will be appended to the prefix automatically. # Path prefix for the provisioning API. /v1 will be appended to the prefix automatically.
apiPrefix: /_matrix/provision apiPrefix: /_matrix/provision
presence:
# Bridge Steam online/offline status
enabled: true
# How often to send status to the homeserver in milliseconds
interval: 5000

File diff suppressed because it is too large Load diff

View file

@ -1,22 +0,0 @@
// 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;
// }
// }
//
//

View file

@ -1,54 +0,0 @@
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
}
}

3421
src/enum.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,15 +7,11 @@ import {
} from "mx-puppet-bridge"; } from "mx-puppet-bridge";
import * as commandLineArgs from "command-line-args"; import * as commandLineArgs from "command-line-args";
import * as commandLineUsage from "command-line-usage"; import * as commandLineUsage from "command-line-usage";
import * as escapeHtml from "escape-html";
import {Steam} from "./steam"; import {Steam} from "./steam";
import {NextcloudConfigWrap} from "./config"; import {NextcloudConfigWrap} from "./config";
import * as fs from "fs"; import * as fs from "fs";
import * as yaml from "js-yaml"; 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 {LoginDetails, LoginToken} from "./login";
import * as SteamID from "steamid";
import * as SteamUser from "steam-user"; import * as SteamUser from "steam-user";
const log = new Log("NextcloudPuppet:index"); const log = new Log("NextcloudPuppet:index");
@ -49,7 +45,7 @@ if (options.help) {
} }
const protocol = { const protocol = {
features: {typingTimeout : 1 * 1000}, features: {typingTimeout : 1000, presence: true},
id: "steam", id: "steam",
displayname: "Steam", displayname: "Steam",
externalUrl: "https://steamcommunity.com/", externalUrl: "https://steamcommunity.com/",

77
src/interfaces.ts Normal file
View file

@ -0,0 +1,77 @@
import SteamId from "steamid";
import {EPersonaState, EChatEntryType, EChatRoomServerMessage} from "./enum";
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 {
persona_state: EPersonaState,
player_name: string,
avatar_hash: Buffer,
last_logoff: Date,
last_logon: Date,
last_seen_online: Date,
game_name: string,
gameid: string,
avatar_url_icon: string,
avatar_url_medium: string,
avatar_url_full: string
}
export interface AppInfo {
changenumber: number,
missingToken: boolean,
appinfo: {
appid: string,
common: {
name: string,
type: string,
releasestate: string,
oslist: string,
logo: string,
logo_small: string,
icon: string,
},
extended: {
developer: string,
publisher: string,
homepage: string,
listofdlc: string
}
}
}

View file

@ -1,20 +1,17 @@
/// <reference path="@types/steam-user/enum.d.ts" />
/// <reference path="@types/steam-user/interfaces.d.ts" />
import { import {
PuppetBridge, IFileEvent,
Log, IMessageEvent,
IReceiveParams, IReceiveParams,
IRemoteRoom, IRemoteRoom,
IRemoteUser, IRemoteUser,
IMessageEvent, Log,
IFileEvent, PuppetBridge,
Util,
IRetList,
} from "mx-puppet-bridge"; } from "mx-puppet-bridge";
import * as SteamUser from "steam-user"; import * as SteamUser from "steam-user";
import * as SteamID from "steamid"; import * as SteamID from "steamid";
import {IIncomingFriendMessage, IPersona} from "steam-user-interfaces"; import {EPersonaState} from "./enum";
import {MatrixPresence} from "mx-puppet-bridge/lib/src/presencehandler";
import {AppInfo, IIncomingFriendMessage, IPersona} from "./interfaces";
const log = new Log("MatrixPuppet:Steam"); const log = new Log("MatrixPuppet:Steam");
@ -23,6 +20,7 @@ interface ISteamPuppet {
data: any; data: any;
sentEventIds: string[]; sentEventIds: string[];
knownPersonas: Map<string, IPersona>, knownPersonas: Map<string, IPersona>,
knownApps: Map<string, AppInfo>,
} }
interface ISteamPuppets { interface ISteamPuppets {
@ -32,6 +30,7 @@ interface ISteamPuppets {
interface IPuppetParams { interface IPuppetParams {
accountName: string, accountName: string,
loginKey: string, loginKey: string,
[key: string]: string; [key: string]: string;
} }
@ -46,7 +45,7 @@ export class Steam {
async getPersona(p: ISteamPuppet, steamid: SteamID): Promise<IPersona> { async getPersona(p: ISteamPuppet, steamid: SteamID): Promise<IPersona> {
let steamIdString = steamid.toString(); let steamIdString = steamid.toString();
let persona = p.knownPersonas.get(steamIdString) let persona = p.knownPersonas.get(steamIdString);
if (persona) { if (persona) {
return persona; return persona;
} else if (p.client.users[steamIdString]) { } else if (p.client.users[steamIdString]) {
@ -59,6 +58,18 @@ export class Steam {
} }
} }
async getProduct(p: ISteamPuppet, appId: string): Promise<AppInfo> {
let app = p.knownApps.get(appId);
if (app) {
return app;
} else {
let {apps} = await p.client.getProductInfo([parseInt(appId, 10)], []);
let app = apps[appId];
p.knownApps.set(appId, app);
return app;
}
}
public async getSendParams(puppetId: number, msg: IIncomingFriendMessage, fromSteamId?: SteamID): Promise<IReceiveParams> { public async getSendParams(puppetId: number, msg: IIncomingFriendMessage, fromSteamId?: SteamID): Promise<IReceiveParams> {
const p = this.puppets[puppetId]; const p = this.puppets[puppetId];
@ -93,6 +104,7 @@ export class Steam {
sentEventIds: [], sentEventIds: [],
typingUsers: {}, typingUsers: {},
knownPersonas: new Map(), knownPersonas: new Map(),
knownApps: new Map(),
} as ISteamPuppet; } as ISteamPuppet;
try { try {
client.logOn({ client.logOn({
@ -101,18 +113,57 @@ export class Steam {
rememberPassword: true, rememberPassword: true,
}); });
client.on("user", (steamId, persona) => { client.on("user", async (steamId, persona: IPersona) => {
this.puppets[puppetId].knownPersonas.set(steamId.toString(), persona); const p = this.puppets[puppetId];
p.knownPersonas.set(steamId.toString(), persona);
let state: MatrixPresence = "offline";
switch (persona.persona_state) {
case EPersonaState.Away:
case EPersonaState.Busy:
case EPersonaState.Snooze:
state = "unavailable";
break;
case EPersonaState.LookingToPlay:
case EPersonaState.LookingToTrade:
case EPersonaState.Online:
state = "online";
break;
}
if (steamId.toString() != client.steamID.toString()) {
await this.bridge.setUserPresence({
puppetId,
userId: steamId.toString()
}, state);
if (persona.gameid && persona.gameid !== '0') {
let app = await this.getProduct(p, persona.gameid);
await this.bridge.setUserStatus({
puppetId,
userId: steamId.toString()
}, `Now playing: ${app.appinfo.common.name}`);
} else {
await this.bridge.setUserStatus({
puppetId,
userId: steamId.toString()
}, "");
}
}
}); });
client.on("loggedOn", async (details) => { client.on("loggedOn", async (details) => {
await this.bridge.setUserId(puppetId, client.steamID.toString()); await this.bridge.setUserId(puppetId, client.steamID.toString());
await this.bridge.sendStatusMessage(puppetId, `connected as ${details.vanity_url}(${client.steamID.toString()})!`); await this.bridge.sendStatusMessage(puppetId, `connected as ${details.vanity_url}(${client.steamID.toString()})!`);
})
client.setPersona(EPersonaState.Away);
});
client.on("loginKey", (loginKey) => { client.on("loginKey", (loginKey) => {
console.log("got new login key"); log.info("got new login key");
data.loginKey = loginKey; data.loginKey = loginKey;
this.bridge.setPuppetData(puppetId, data); this.bridge.setPuppetData(puppetId, data);
}); });
@ -125,7 +176,7 @@ export class Steam {
}); });
client.chat.on("friendTyping", (message: IIncomingFriendMessage) => { client.chat.on("friendTyping", (message: IIncomingFriendMessage) => {
this.handleFriendTyping(puppetId, message); this.handleFriendTyping(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);
@ -171,7 +222,6 @@ export class Steam {
}, true); }, true);
} }
public async sendMessageToSteam( public async sendMessageToSteam(
p: ISteamPuppet, p: ISteamPuppet,
room: IRemoteRoom, room: IRemoteRoom,
@ -179,22 +229,20 @@ export class Steam {
msg: string, msg: string,
mediaId?: string, mediaId?: string,
) { ) {
let id = "";
try { try {
let _roomSteamId = new SteamID(room.name as string); let _roomSteamId = new SteamID(room.roomId as string);
if (_roomSteamId.isValid()) { if (_roomSteamId.isValid()) {
const sendMessage = await p.client.chat.sendFriendMessage(room.roomId, msg); const sendMessage = await p.client.chat.sendFriendMessage(room.roomId, msg);
id = `${sendMessage.server_timestamp.toISOString()}::${sendMessage.ordinal}`; let id = `${sendMessage.server_timestamp.toISOString()}::${sendMessage.ordinal}`;
await this.bridge.eventSync.insert(room, eventId, id);
p.sentEventIds.push(id);
} else { } else {
await this.bridge.sendStatusMessage(room.puppetId, `Sending group messages is currently not supported`); await this.bridge.sendStatusMessage(room.puppetId, `Sending group messages is currently not supported`);
} }
} catch (e) { } catch (e) {
await this.bridge.sendStatusMessage(room.puppetId, `Sending group messages is currently not supported`); 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) { public async handleMatrixMessage(room: IRemoteRoom, data: IMessageEvent, event: any) {
@ -228,11 +276,15 @@ export class Steam {
if (!p) { if (!p) {
return null; return null;
} }
let persona = await this.getPersona(p, new SteamID(user.userId));
log.info(`Got request to create user ${user.userId}`); log.info(`Got request to create user ${user.userId}`);
return { return {
userId: user.userId, userId: user.userId,
puppetId: user.puppetId, puppetId: user.puppetId,
name: user.name, name: persona.player_name,
avatarUrl: persona.avatar_url_medium
}; };
} }

View file

@ -1,84 +0,0 @@
import * as SteamUser from "steam-user";
import {log} from "util";
import SteamID = require("steamid");
const SteamCommunity = require('steamcommunity');
let community = new SteamCommunity();
let user = new SteamUser() as any;
function tryLogin(details): Promise<void> {
return new Promise((res, rej) => community.login(details, function(err) {
if (err) {
rej(err);
} else {
res();
}
}));
}
interface LoginDetails {
"accountName"?: string,
"password"?: string,
"steamguard"?: string,
"authCode"?: string,
"twoFactorCode"?: string,
"captcha"?: string,
"disableMobile"?: boolean,
}
interface LoginToken {
accountName: string,
webLoginToken: string,
steamID: SteamID
}
async function login(details: LoginDetails, steamGuard?: () => Promise<string>, twoFactor?: () => Promise<string>, captcha?: (url: string) => Promise<string>): Promise<LoginToken> {
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
}
}
}
}
async function main() {
let details = {
"accountName": "icewind1991",
"password": ""
};
console.log(await login(details, undefined, async () => "WM3MR"));
}
main();