mirror of
https://github.com/icewind1991/mx-puppet-steam.git
synced 2026-06-03 17:44:09 +02:00
better gameinvite
This commit is contained in:
parent
09ea312a47
commit
3656419a80
4 changed files with 301 additions and 35 deletions
288
package-lock.json
generated
288
package-lock.json
generated
|
|
@ -267,29 +267,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.10.tgz",
|
||||||
"integrity": "sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ=="
|
"integrity": "sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ=="
|
||||||
},
|
},
|
||||||
"@types/node-fetch": {
|
|
||||||
"version": "2.5.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz",
|
|
||||||
"integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*",
|
|
||||||
"form-data": "^3.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"form-data": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.5",
|
"version": "6.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
|
||||||
|
|
@ -585,6 +562,11 @@
|
||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
|
||||||
|
},
|
||||||
"buffer-writer": {
|
"buffer-writer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
||||||
|
|
@ -2119,11 +2101,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/node-bignumber/-/node-bignumber-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-bignumber/-/node-bignumber-1.2.1.tgz",
|
||||||
"integrity": "sha1-JmyVUzUoOFOfZhyS5YYxvpeRfqU="
|
"integrity": "sha1-JmyVUzUoOFOfZhyS5YYxvpeRfqU="
|
||||||
},
|
},
|
||||||
"node-fetch": {
|
|
||||||
"version": "2.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
|
||||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
|
||||||
},
|
|
||||||
"node-pre-gyp": {
|
"node-pre-gyp": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
|
||||||
|
|
@ -2151,6 +2128,256 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node-steam": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-steam/-/node-steam-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-GSiwEUzp5MaUJDRO6qWZzekeNCQ=",
|
||||||
|
"requires": {
|
||||||
|
"adm-zip": "^0.4",
|
||||||
|
"buffer-crc32": "^0.2",
|
||||||
|
"bytebuffer": "^5.0",
|
||||||
|
"steam-crypto": "^0.0",
|
||||||
|
"steam-resources": "git+https://github.com/seishun/node-steam-resources.git#v1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"steam-resources": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"from": "git+https://github.com/seishun/node-steam-resources.git#v1.0.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"bytebuffer": "^5.0",
|
||||||
|
"protobufjs": "^4.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"ascli": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"colour": "~0.7.1",
|
||||||
|
"optjs": "~3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^0.4.1",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bufferview": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"bytebuffer": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"long": "~3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"camelcase": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"cliui": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"string-width": "^1.0.1",
|
||||||
|
"strip-ansi": "^3.0.1",
|
||||||
|
"wrap-ansi": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code-point-at": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"colour": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "5.0.15",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "2 || 3",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"invert-kv": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"is-fullwidth-code-point": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"number-is-nan": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lcid": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"invert-kv": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"long": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number-is-nan": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optjs": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"os-locale": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"lcid": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"protobufjs": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"ascli": "~1",
|
||||||
|
"bytebuffer": "~4 >=4.1",
|
||||||
|
"glob": "^5.0.10",
|
||||||
|
"yargs": "^3.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bytebuffer": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"bufferview": "~1",
|
||||||
|
"long": "~2 >=2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"long": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"bundled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"string-width": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"code-point-at": "^1.0.0",
|
||||||
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
"strip-ansi": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-ansi": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-regex": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"window-size": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"wrap-ansi": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"string-width": "^1.0.1",
|
||||||
|
"strip-ansi": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"y18n": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"bundled": true
|
||||||
|
},
|
||||||
|
"yargs": {
|
||||||
|
"version": "3.32.0",
|
||||||
|
"bundled": true,
|
||||||
|
"requires": {
|
||||||
|
"camelcase": "^2.0.1",
|
||||||
|
"cliui": "^3.0.3",
|
||||||
|
"decamelize": "^1.1.1",
|
||||||
|
"os-locale": "^1.4.0",
|
||||||
|
"string-width": "^1.0.1",
|
||||||
|
"window-size": "^0.1.4",
|
||||||
|
"y18n": "^3.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node-steam-user": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-steam-user/-/node-steam-user-0.0.3.tgz",
|
||||||
|
"integrity": "sha1-Pcy8L9r0MHVLQDafq53laVa8Pkw=",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4",
|
||||||
|
"node-steam": "*",
|
||||||
|
"steamid": "^1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"noop-logger": {
|
"noop-logger": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
|
||||||
|
|
@ -2868,6 +3095,11 @@
|
||||||
"steamid": "^1.1.0"
|
"steamid": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"steam-crypto": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/steam-crypto/-/steam-crypto-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-BHexgqKx/dlBiT28wi4Ok7FKu38="
|
||||||
|
},
|
||||||
"steam-totp": {
|
"steam-totp": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/steam-totp/-/steam-totp-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/steam-totp/-/steam-totp-2.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"gif-encoder": "^0.7.2",
|
"gif-encoder": "^0.7.2",
|
||||||
"js-yaml": "^3.14.0",
|
"js-yaml": "^3.14.0",
|
||||||
"mx-puppet-bridge": "0.1.0-1",
|
"mx-puppet-bridge": "0.1.0-1",
|
||||||
|
"node-steam-user": "0.0.3",
|
||||||
"steam-user": "icewind1991/node-steam-user#bbcode-sticker",
|
"steam-user": "icewind1991/node-steam-user#bbcode-sticker",
|
||||||
"steamcommunity": "3.42.0",
|
"steamcommunity": "3.42.0",
|
||||||
"steamid": "1.1.3",
|
"steamid": "1.1.3",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import UPNG from "@pdf-lib/upng";
|
||||||
const GIFEncoder = require("gif-encoder");
|
const GIFEncoder = require("gif-encoder");
|
||||||
import {WritableStreamBuffer} from 'stream-buffers';
|
import {WritableStreamBuffer} from 'stream-buffers';
|
||||||
import {Util} from "mx-puppet-bridge";
|
import {Util} from "mx-puppet-bridge";
|
||||||
|
import * as SteamID from "steamid";
|
||||||
|
|
||||||
function isBBCode(field: BBCodeField): field is BBCodeNode {
|
function isBBCode(field: BBCodeField): field is BBCodeNode {
|
||||||
return field['tag'] !== undefined;
|
return field['tag'] !== undefined;
|
||||||
|
|
@ -45,8 +46,7 @@ async function apngToGif(sourceUrl: string): Promise<Buffer> {
|
||||||
return output.getContents();
|
return output.getContents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function formatBBCode(steam: Steam, puppetId: number, node: BBCodeNode, fromSteamId?: SteamID): Promise<ImageMessage | TextMessage> {
|
||||||
async function formatBBCode(steam: Steam, puppetId: number, node: BBCodeNode): Promise<ImageMessage | TextMessage> {
|
|
||||||
if (node.tag === 'img') {
|
if (node.tag === 'img') {
|
||||||
return {
|
return {
|
||||||
kind: "image",
|
kind: "image",
|
||||||
|
|
@ -69,22 +69,45 @@ async function formatBBCode(steam: Steam, puppetId: number, node: BBCodeNode): P
|
||||||
kind: "image",
|
kind: "image",
|
||||||
urlOrBuffer: gif
|
urlOrBuffer: gif
|
||||||
};
|
};
|
||||||
|
} else if (node.tag === 'gameinvite') {
|
||||||
|
let game = await steam.getProduct(puppetId, node.attrs['appid']);
|
||||||
|
|
||||||
|
if (steam.getSteamId(puppetId) === fromSteamId) {
|
||||||
|
return {
|
||||||
|
kind: "text",
|
||||||
|
body: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "text",
|
||||||
|
body: `You were invited to play ${game.appinfo.common.name}`
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {kind: "text", body: `[${node.tag}]`};
|
return {kind: "text", body: `[${node.tag}]`};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportMessageForSending(steam: Steam, puppetId: number, message: IIncomingFriendMessage | IIncomingChatMessage): Promise<(TextMessage | ImageMessage)[]> {
|
export async function exportMessageForSending(
|
||||||
|
steam: Steam,
|
||||||
|
puppetId: number,
|
||||||
|
message: IIncomingFriendMessage | IIncomingChatMessage,
|
||||||
|
fromSteamId?: SteamID
|
||||||
|
): Promise<(TextMessage | ImageMessage)[]> {
|
||||||
if (message.message_bbcode_parsed) {
|
if (message.message_bbcode_parsed) {
|
||||||
let parts = await Promise.all(message.message_bbcode_parsed.map(node => {
|
let parts = await Promise.all(message.message_bbcode_parsed.map(node => {
|
||||||
if (isBBCode(node)) {
|
if (isBBCode(node)) {
|
||||||
return formatBBCode(steam, puppetId, node);
|
return formatBBCode(steam, puppetId, node, message['steamid_friend']);
|
||||||
} else {
|
} else {
|
||||||
return {kind: "text", body: node} as TextMessage;
|
return {kind: "text", body: node} as TextMessage;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return parts.reduce((merged, part) => {
|
return parts.reduce((merged, part) => {
|
||||||
|
if (part.kind === "text" && part.body === "") {
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
if (merged.length === 0) {
|
if (merged.length === 0) {
|
||||||
merged.push(part);
|
merged.push(part);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
16
src/steam.ts
16
src/steam.ts
|
|
@ -74,7 +74,8 @@ export class Steam {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProduct(p: ISteamPuppet, appId: string): Promise<AppInfo> {
|
public async getProduct(puppetId: number, appId: string): Promise<AppInfo> {
|
||||||
|
const p = this.puppets[puppetId];
|
||||||
let app = p.knownApps.get(appId);
|
let app = p.knownApps.get(appId);
|
||||||
if (app) {
|
if (app) {
|
||||||
return app;
|
return app;
|
||||||
|
|
@ -140,6 +141,10 @@ export class Steam {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSteamId(puppetId: number): SteamID | null {
|
||||||
|
return this.puppets[puppetId].client.steamID;
|
||||||
|
}
|
||||||
|
|
||||||
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]) {
|
||||||
|
|
@ -291,12 +296,17 @@ export class Steam {
|
||||||
const p = this.puppets[puppetId];
|
const p = this.puppets[puppetId];
|
||||||
log.verbose("Got chat message from steam to pass on");
|
log.verbose("Got chat message from steam to pass on");
|
||||||
|
|
||||||
let sendParams = await this.getChatMessageSendParams(puppetId, message, fromSteamId);
|
let sendParams = await this.getChatMessageSendParams(puppetId, message);
|
||||||
|
|
||||||
await this.sendMessage(p, puppetId, sendParams, message);
|
await this.sendMessage(p, puppetId, sendParams, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendMessage(puppet: ISteamPuppet, puppetId: number, sendParams: IReceiveParams, incoming: IIncomingFriendMessage | IIncomingChatMessage) {
|
public async sendMessage(
|
||||||
|
puppet: ISteamPuppet,
|
||||||
|
puppetId: number,
|
||||||
|
sendParams: IReceiveParams,
|
||||||
|
incoming: IIncomingFriendMessage | IIncomingChatMessage
|
||||||
|
) {
|
||||||
const parts = await exportMessageForSending(this, puppetId, incoming);
|
const parts = await exportMessageForSending(this, puppetId, incoming);
|
||||||
|
|
||||||
for (let part of parts) {
|
for (let part of parts) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue