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
The bridge is in early status, logging in and 1<->1 messages are mostly working.
Group messages are not working, typing status works somewhat.
- [x] login with steam guard support
- [x] 1 <->1 messaging
- [ ] group messaging
- [x] steam -> matrix typing notifications
- [x] online/offline status
- [x] retrieve nickname and avatar from steam
## Linking
@ -15,4 +19,4 @@ Start a chat with @_steampuppet_bot:yourserver.com
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",
"dependencies": {
"body-parser": "^1.19.0",
"command-line-args": "^5.1.1",
"command-line-usage": "^5.0.5",
"js-yaml": "^3.13.1",
"mx-puppet-bridge": "0.0.40",
"mx-puppet-bridge": "0.0.43",
"steam-user": "4.15.1",
"steamcommunity": "3.41.3",
"steamid": "1.1.3",
"tslint": "^5.20.1",
"typescript": "^3.8.3"
},
"devDependencies": {

View file

@ -22,3 +22,10 @@ provisioning:
#sharedSecret: random string
# Path prefix for the provisioning API. /v1 will be appended to the prefix automatically.
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";
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");
@ -49,7 +45,7 @@ if (options.help) {
}
const protocol = {
features: {typingTimeout : 1 * 1000},
features: {typingTimeout : 1000, presence: true},
id: "steam",
displayname: "Steam",
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 {
PuppetBridge,
Log,
IFileEvent,
IMessageEvent,
IReceiveParams,
IRemoteRoom,
IRemoteUser,
IMessageEvent,
IFileEvent,
Util,
IRetList,
Log,
PuppetBridge,
} from "mx-puppet-bridge";
import * as SteamUser from "steam-user";
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");
@ -23,6 +20,7 @@ interface ISteamPuppet {
data: any;
sentEventIds: string[];
knownPersonas: Map<string, IPersona>,
knownApps: Map<string, AppInfo>,
}
interface ISteamPuppets {
@ -32,6 +30,7 @@ interface ISteamPuppets {
interface IPuppetParams {
accountName: string,
loginKey: string,
[key: string]: string;
}
@ -46,7 +45,7 @@ export class Steam {
async getPersona(p: ISteamPuppet, steamid: SteamID): Promise<IPersona> {
let steamIdString = steamid.toString();
let persona = p.knownPersonas.get(steamIdString)
let persona = p.knownPersonas.get(steamIdString);
if (persona) {
return persona;
} 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> {
const p = this.puppets[puppetId];
@ -93,6 +104,7 @@ export class Steam {
sentEventIds: [],
typingUsers: {},
knownPersonas: new Map(),
knownApps: new Map(),
} as ISteamPuppet;
try {
client.logOn({
@ -101,18 +113,57 @@ export class Steam {
rememberPassword: true,
});
client.on("user", (steamId, persona) => {
this.puppets[puppetId].knownPersonas.set(steamId.toString(), persona);
client.on("user", async (steamId, persona: IPersona) => {
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) => {
await this.bridge.setUserId(puppetId, client.steamID.toString());
await this.bridge.sendStatusMessage(puppetId, `connected as ${details.vanity_url}(${client.steamID.toString()})!`);
})
client.setPersona(EPersonaState.Away);
});
client.on("loginKey", (loginKey) => {
console.log("got new login key");
log.info("got new login key");
data.loginKey = loginKey;
this.bridge.setPuppetData(puppetId, data);
});
@ -125,7 +176,7 @@ export class Steam {
});
client.chat.on("friendTyping", (message: IIncomingFriendMessage) => {
this.handleFriendTyping(puppetId, message);
})
});
client.on("error", (err) => {
log.error(`Failed to start up puppet ${puppetId}`, err);
@ -171,7 +222,6 @@ export class Steam {
}, true);
}
public async sendMessageToSteam(
p: ISteamPuppet,
room: IRemoteRoom,
@ -179,22 +229,20 @@ export class Steam {
msg: string,
mediaId?: string,
) {
let id = "";
try {
let _roomSteamId = new SteamID(room.name as string);
let _roomSteamId = new SteamID(room.roomId as string);
if (_roomSteamId.isValid()) {
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 {
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) {
@ -228,11 +276,15 @@ export class Steam {
if (!p) {
return null;
}
let persona = await this.getPersona(p, new SteamID(user.userId));
log.info(`Got request to create user ${user.userId}`);
return {
userId: user.userId,
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();