a
This commit is contained in:
commit
92ac630a45
5
.dockerignore
Normal file
5
.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.git
|
||||||
|
Dockerfile
|
||||||
|
node_modules
|
||||||
|
*.db
|
||||||
|
data
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
*.db
|
||||||
|
data
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
FROM node:lts-slim
|
||||||
|
RUN apt update && apt install -y tini
|
||||||
|
ENTRYPOINT ["tini", "--", "node", "."]
|
||||||
|
|
||||||
|
COPY --chown=node:node . /appservice
|
||||||
|
WORKDIR /appservice
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
RUN npm i --no-audit --no-fund
|
||||||
|
CMD ["-p", "8000", "-c", "data/xss.yaml"]
|
7
LICENSE
Normal file
7
LICENSE
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
The Software shall be used for Evil, not Good.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
81
README.md
Normal file
81
README.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# xssbook to matrix bridge
|
||||||
|
|
||||||
|
references:
|
||||||
|
- [matrix-appservice-bridge howto](https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/HOWTO.md)
|
||||||
|
- [the slack bridge](https://github.com/matrix-org/matrix-appservice-slack)
|
||||||
|
|
||||||
|
## usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir data # if it doesn't already exist
|
||||||
|
node . -r -u "http://url-to-appservice:8000" # the url here is how the homeserver will try to access the appservice, the port is your choice
|
||||||
|
```
|
||||||
|
this generates a registration file `data/xss-registration.yaml` that you use to register the bridge with a matrix homeserver
|
||||||
|
install the app service like [this](https://docs.mau.fi/bridges/general/registering-appservices.html)
|
||||||
|
|
||||||
|
then make a `data/xss.yaml` file:
|
||||||
|
```yaml
|
||||||
|
xssbook:
|
||||||
|
url: http://localhost:8080 # url to xssbook instance
|
||||||
|
bridge_username: bridge # name of the bridge user (used to poll for new posts)
|
||||||
|
room: "!roomid:localhost" # matrix room to post to
|
||||||
|
matrix:
|
||||||
|
homeserver_url: http://localhost:8008 # http(s) url to your homeserver
|
||||||
|
homeserver: localhost # what appears in the second half of matrix IDs
|
||||||
|
```
|
||||||
|
and run
|
||||||
|
```sh
|
||||||
|
node . -p 8000 -c data/xss.yaml
|
||||||
|
# the port needs to be the same one from the previous node command, if bridging to matrix
|
||||||
|
```
|
||||||
|
|
||||||
|
that should be it.
|
||||||
|
|
||||||
|
## if you want to use docker instead
|
||||||
|
|
||||||
|
be aware of networking shenanigans, especially if you're running this on the same machine as the homeserver.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t matrixssbook . # make sure the image is built
|
||||||
|
mkdir data # you *may* need to chmod a+w data to allow the node user inside the container to write to the folder
|
||||||
|
|
||||||
|
docker run --rm -v $PWD/data:/appservice/data matrixssbook -r -u "http://url-to-appservice:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
create the `data/xss.yaml` as before
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run -v $PWD/data:/appservice/data matrixssbook # note: the default cmd is `-p 8000 -c data/xss.yaml`, if you used a different port you'll need to include parameters here
|
||||||
|
```
|
||||||
|
|
||||||
|
## docker-compose
|
||||||
|
|
||||||
|
if you're running [synapse through docker-compose](https://github.com/matrix-org/synapse/blob/master/contrib/docker/docker-compose.yml), you can add this appservice to your `docker-compose.yaml` file:
|
||||||
|
```sh
|
||||||
|
docker build -t matrixssbook /path/to/this/repo # make sure the image is built
|
||||||
|
# run in the same folder as your docker-compose file
|
||||||
|
mkdir data # you *may* need to chmod a+w data to allow the node user inside the container to write to the folder
|
||||||
|
docker run --rm -v $PWD/data:/appservice/data matrixssbook -r -u "http://matrixssbook:8000" # this url can be used verbatim
|
||||||
|
```
|
||||||
|
create the `data/xss.yaml` file as before, but use `homeserver_url: http://synapse:8008`
|
||||||
|
then insert this snippet into your `docker-compose.yaml`
|
||||||
|
```yaml
|
||||||
|
# in synapse's volumes section, use this (optional, you can also just copy the file into synapse's data directory manually)
|
||||||
|
- ./data/xss-registration.yaml:/data/xss-registration.yaml
|
||||||
|
# add a new service
|
||||||
|
matrixssbook:
|
||||||
|
image: matrixssbook
|
||||||
|
volumes:
|
||||||
|
- ./data:/appservice/data
|
||||||
|
```
|
||||||
|
make sure to edit synapse's config to add
|
||||||
|
```yaml
|
||||||
|
app_service_config_files:
|
||||||
|
- /data/xss-registration.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## using with matrix
|
||||||
|
|
||||||
|
- make a new room, make sure `xss.room` in the bridge's config is set to the room's ID
|
||||||
|
- invite `@xssbook:homeserver`, it should automatically accept the invite
|
||||||
|
- start posting
|
72
index.js
Normal file
72
index.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
const { MxBridge } = require("./mxbridge");
|
||||||
|
const { store } = require("./store");
|
||||||
|
const { XSSBook } = require("./xssbook.js");
|
||||||
|
|
||||||
|
// create the matrix bridge
|
||||||
|
const mxbridge = new MxBridge(
|
||||||
|
"data/xss-registration.yaml",
|
||||||
|
"xss-config-schema.yaml",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
localpart: "xssbook",
|
||||||
|
regex: [
|
||||||
|
{ type: "users", regex: "@xss_.*", exclusive: true },
|
||||||
|
{ type: "aliases", regex: "#xss_.*", exclusive: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
config => ({
|
||||||
|
homeserver: config.matrix?.homeserver,
|
||||||
|
homeserver_url: config.matrix?.homeserver_url,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// persist uploaded avatar urls
|
||||||
|
const avatarStore = store("data/avatars.json");
|
||||||
|
mxbridge.avatars = avatarStore.store;
|
||||||
|
// save on change
|
||||||
|
mxbridge.on("avatarupload", () => {
|
||||||
|
avatarStore.save().catch(err => {
|
||||||
|
console.error("Error writing avatar store", err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mxbridge.on("config", config => {
|
||||||
|
const xss = new XSSBook({
|
||||||
|
...config.xssbook,
|
||||||
|
});
|
||||||
|
// store associations for messages sent by the bridge so it can mirror edits and deletes
|
||||||
|
const matrixToXss = {};
|
||||||
|
const xssToMatrix = {};
|
||||||
|
|
||||||
|
// xssbook -> matrix
|
||||||
|
|
||||||
|
xss.on("post", async (post, user) => {
|
||||||
|
if(post.post_id in xssToMatrix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xssToMatrix[post.post_id] = await mxbridge.sendMessage(
|
||||||
|
xss.room,
|
||||||
|
{
|
||||||
|
localpart: "xss_" + post.user_id,
|
||||||
|
username: user.firstname + " " + user.lastname,
|
||||||
|
avatar: xss.url + "/image/avatar?user_id=" + post.user_id,
|
||||||
|
},
|
||||||
|
post.content,
|
||||||
|
post.content,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// matrix -> xssbook
|
||||||
|
|
||||||
|
mxbridge.on("message", async (content, evt, bridge) => {
|
||||||
|
if(evt.room_id !== xss.room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixToXss[evt.event_id] = await xss.post(evt.sender, content.formatted_body || content.body);
|
||||||
|
xssToMatrix[matrixToXss[evt.event_id]] = evt.event_id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", reason => {
|
||||||
|
console.log("Unhandled rejection:", reason);
|
||||||
|
});
|
306
mxbridge.js
Normal file
306
mxbridge.js
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
const { Cli, Bridge, WeakEvent, Intent, AppServiceRegistration } = require("matrix-appservice-bridge");
|
||||||
|
const { LogService } = require("matrix-bot-sdk");
|
||||||
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* localpart?: string,
|
||||||
|
* regex?: {type: "users"|"rooms"|"aliases", regex: string, exclusive?: boolean}[],
|
||||||
|
* }} Registration
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* localpart: string,
|
||||||
|
* username?: string,
|
||||||
|
* avatar?: string,
|
||||||
|
* }} User
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {(config: Record<string, number>) => {
|
||||||
|
* homeserver: string,
|
||||||
|
* homeserver_url: string,
|
||||||
|
* }} EvalConfig
|
||||||
|
*/
|
||||||
|
const defaultEvalConfig = config => ({
|
||||||
|
enabled: true,
|
||||||
|
homeserver: config.homeserver,
|
||||||
|
homeserver_url: config.homeserver_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports.MxBridge = class MxBridge extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* @param {string} registrationPath
|
||||||
|
* @param {string} schema
|
||||||
|
* @param {Record<string, unknown>} defaults
|
||||||
|
* @param {Registration} registration
|
||||||
|
* @param {EvalConfig} getValues
|
||||||
|
*/
|
||||||
|
constructor(registrationPath, schema, defaults, registration, getValues = defaultEvalConfig) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
let bridgeResolve;
|
||||||
|
/** @type {Promise<Bridge>} */
|
||||||
|
this.bridge = new Promise(r => bridgeResolve = r);
|
||||||
|
|
||||||
|
this.avatars = {};
|
||||||
|
|
||||||
|
new Cli({
|
||||||
|
registrationPath,
|
||||||
|
bridgeConfig: {
|
||||||
|
schema,
|
||||||
|
defaults,
|
||||||
|
},
|
||||||
|
generateRegistration(reg, cb) {
|
||||||
|
reg.setId(AppServiceRegistration.generateToken());
|
||||||
|
reg.setHomeserverToken(AppServiceRegistration.generateToken());
|
||||||
|
reg.setAppServiceToken(AppServiceRegistration.generateToken());
|
||||||
|
if(registration.localpart) {
|
||||||
|
reg.setSenderLocalpart(registration.localpart);
|
||||||
|
}
|
||||||
|
if(Array.isArray(registration.regex)) {
|
||||||
|
registration.regex.forEach(r => {
|
||||||
|
reg.addRegexPattern(r.type, r.regex, r.exclusive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cb(reg);
|
||||||
|
},
|
||||||
|
run: async (port, config) => {
|
||||||
|
// don't emit "config" synchronously, give a consumer a chance to attach a listener for it
|
||||||
|
await new Promise(r => setImmediate(r));
|
||||||
|
this.emit("config", config);
|
||||||
|
|
||||||
|
const values = getValues(config);
|
||||||
|
|
||||||
|
const bridge = new Bridge({
|
||||||
|
homeserverUrl: values.homeserver_url,
|
||||||
|
domain: values.homeserver,
|
||||||
|
registration: registrationPath,
|
||||||
|
disableStores: true,
|
||||||
|
controller: {
|
||||||
|
onUserQuery(u) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onEvent: async (req, ctx) => {
|
||||||
|
const evt = req.getData();
|
||||||
|
// console.log("got event", evt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @event MxBridge#evt
|
||||||
|
* @param {WeakEvent} event
|
||||||
|
* @param {Bridge} bridge
|
||||||
|
*/
|
||||||
|
this.emit("evt", evt, bridge);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("Matrix appservice listening on port", port);
|
||||||
|
await bridge.run(port);
|
||||||
|
bridgeResolve(bridge);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @event MxBridge#login
|
||||||
|
* @param {Bridge} bridge
|
||||||
|
* @param {Record<string, unknown>} config
|
||||||
|
*/
|
||||||
|
this.emit("login", bridge, config);
|
||||||
|
}
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
this.on("evt", async (evt, bridge) => {
|
||||||
|
// the bridge user should autojoin on invites
|
||||||
|
if(
|
||||||
|
evt.type === "m.room.member" &&
|
||||||
|
evt.state_key === bridge.getBot().getUserId() &&
|
||||||
|
evt.content?.membership === "invite"
|
||||||
|
) {
|
||||||
|
console.log("got invite! joining")
|
||||||
|
await bridge.getIntent().join(evt.room_id);
|
||||||
|
/**
|
||||||
|
* @event MxBridge#room
|
||||||
|
* @param {string} room_id
|
||||||
|
* @param {Bridge} bridge
|
||||||
|
*/
|
||||||
|
this.emit("room", evt.room_id, bridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
evt.type === "m.room.message" &&
|
||||||
|
evt.content
|
||||||
|
) {
|
||||||
|
if(evt.content["m.relates_to"]?.["rel_type"] === "m.replace" && evt.content["m.new_content"]) {
|
||||||
|
/**
|
||||||
|
* @event MxBridge#edit
|
||||||
|
* @param {object} content
|
||||||
|
* @param {string} replaces
|
||||||
|
* @param {WeakEvent} event
|
||||||
|
* @param {Bridge} bridge
|
||||||
|
*/
|
||||||
|
this.emit("edit", evt.content["m.new_content"], evt.content["m.relates_to"].event_id, evt, bridge);
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* @event MxBridge#message
|
||||||
|
* @param {object} content
|
||||||
|
* @param {WeakEvent} event
|
||||||
|
* @param {Bridge} bridge
|
||||||
|
*/
|
||||||
|
this.emit("message", evt.content, evt, bridge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
evt.type === "m.room.redaction" &&
|
||||||
|
evt.redacts
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* @event MxBridge#redact
|
||||||
|
* @param {string} redacts
|
||||||
|
* @param {WeakEvent} event
|
||||||
|
* @param {Bridge} bridge
|
||||||
|
*/
|
||||||
|
this.emit("redact", evt.redacts, evt, bridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
evt.type === "m.reaction" &&
|
||||||
|
evt.content?.["m.relates_to"]?.rel_type === "m.annotation"
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* @event MxBridge#react
|
||||||
|
* @param {string} reacts_to
|
||||||
|
* @param {string} reaction
|
||||||
|
* @param {WeakEvent} event
|
||||||
|
* @param {Bridge} bridge
|
||||||
|
*/
|
||||||
|
this.emit("react", evt.content["m.relates_to"].event_id, evt.content["m.relates_to"].key, evt, bridge);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a user's avatar to the content repository if it isn't already
|
||||||
|
* @param {string} url
|
||||||
|
* @param {Intent} intent
|
||||||
|
*/
|
||||||
|
async ensureUploaded(url, intent) {
|
||||||
|
if(url in this.avatars) {
|
||||||
|
return this.avatars[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("fetching file", url);
|
||||||
|
const res = await fetch(url);
|
||||||
|
const blob = await res.blob();
|
||||||
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
||||||
|
|
||||||
|
console.log("uploading to matrix: content type", blob.type);
|
||||||
|
const mxc = await intent.uploadContent(buffer, blob.type);
|
||||||
|
|
||||||
|
console.log("got mxc url", mxc);
|
||||||
|
this.avatars[url] = mxc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @event MxBridge#avatarupload
|
||||||
|
* @param {string} mxc Matrix content repository URL
|
||||||
|
* @param {string} url Original HTTP URL
|
||||||
|
* @param {{[key: string]: string}} avatars The current avatar cache
|
||||||
|
*/
|
||||||
|
this.emit("avatarupload", mxc, url, this.avatars);
|
||||||
|
return mxc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} room
|
||||||
|
* @param {User} user
|
||||||
|
* @param {string} body
|
||||||
|
* @param {string?} formatted_body
|
||||||
|
*/
|
||||||
|
async sendMessage(room, user, body, formatted_body) {
|
||||||
|
const bridge = await this.bridge;
|
||||||
|
const intent = bridge.getIntentFromLocalpart(user.localpart);
|
||||||
|
|
||||||
|
let avatar;
|
||||||
|
if(user.avatar) {
|
||||||
|
avatar = await this.ensureUploaded(user.avatar, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
await intent.setRoomUserProfile(room, {
|
||||||
|
displayname: user.username,
|
||||||
|
avatar_url: avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
// look. this error. it's annoying. and it is of no consequence.
|
||||||
|
// the user already existing is the *goal*. but there's no switch to turn it off.
|
||||||
|
// so i'm making one, ok?
|
||||||
|
const oldError = LogService.error;
|
||||||
|
LogService.error = function(logType, msg, err) {
|
||||||
|
if(logType === "Appservice" && msg === "Error registering user: User ID is in use") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(logType === "MatrixHttpClient" && msg.startsWith("(REQ-") && err.errcode === "M_USER_IN_USE") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return oldError.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { event_id } = await intent.sendMessage(room, {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body,
|
||||||
|
...(formatted_body !== undefined ? {
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body
|
||||||
|
} : {})
|
||||||
|
})
|
||||||
|
|
||||||
|
// put that back where it came from or so help me
|
||||||
|
LogService.error = oldError;
|
||||||
|
|
||||||
|
return event_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} room
|
||||||
|
* @param {User} user
|
||||||
|
* @param {string} eventId
|
||||||
|
* @param {string} body
|
||||||
|
* @param {string?} formatted_body
|
||||||
|
*/
|
||||||
|
async editMessage(room, user, eventId, body, formatted_body) {
|
||||||
|
const bridge = await this.bridge;
|
||||||
|
const intent = bridge.getIntentFromLocalpart(user.localpart);
|
||||||
|
const { event_id } = intent.sendMessage(room, {
|
||||||
|
body: "* " + body,
|
||||||
|
msgtype: "m.text",
|
||||||
|
"m.new_content": {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body,
|
||||||
|
...(formatted_body !== undefined ? {
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body
|
||||||
|
} : {})
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: eventId,
|
||||||
|
rel_type: "m.replace",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return event_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} room
|
||||||
|
* @param {User} user
|
||||||
|
* @param {string} event_id
|
||||||
|
*/
|
||||||
|
async redact(room, user, event_id) {
|
||||||
|
const bridge = await this.bridge;
|
||||||
|
const intent = bridge.getIntentFromLocalpart(user.localpart);
|
||||||
|
return await intent.matrixClient.redactEvent(room, event_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMxcToHttp() {
|
||||||
|
const bridge = await this.bridge;
|
||||||
|
return url => bridge.getBot().getClient().mxcToHttp(url);
|
||||||
|
}
|
||||||
|
}
|
4201
package-lock.json
generated
Normal file
4201
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
15
package.json
Normal file
15
package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "matrixssbook",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"matrix-appservice-bridge": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
18
store.js
Normal file
18
store.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports.store = fname => {
|
||||||
|
const basename = fname;
|
||||||
|
fname = path.join(__dirname, fname);
|
||||||
|
let store = {};
|
||||||
|
try {
|
||||||
|
store = JSON.parse(fs.readFileSync(fname).toString());
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
save: () => {
|
||||||
|
return fs.promises.writeFile(fname, JSON.stringify(store));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
xss-config-schema.yaml
Normal file
21
xss-config-schema.yaml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
type: object
|
||||||
|
requires: ["xssbook"]
|
||||||
|
properties:
|
||||||
|
xssbook:
|
||||||
|
type: object
|
||||||
|
requires: ["url", "bridge_username", "room"]
|
||||||
|
properties:
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
bridge_username:
|
||||||
|
type: string
|
||||||
|
room:
|
||||||
|
type: string
|
||||||
|
matrix:
|
||||||
|
type: object
|
||||||
|
requires: ["homeserver", "homeserver_url"]
|
||||||
|
properties:
|
||||||
|
homeserver:
|
||||||
|
type: string
|
||||||
|
homeserver_url:
|
||||||
|
type: string
|
122
xssbook.js
Normal file
122
xssbook.js
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
|
module.exports.XSSBook = class XSSBook extends EventEmitter {
|
||||||
|
constructor({ url, bridge_username, room }) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
|
this.bridge_username = bridge_username;
|
||||||
|
this.room = room;
|
||||||
|
|
||||||
|
this.tokens = {};
|
||||||
|
this.posts = {};
|
||||||
|
|
||||||
|
this.pollLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
async api(url, method, data, token) {
|
||||||
|
return await fetch(this.url + url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"Cookie": token ? "auth=" + encodeURIComponent(token) : undefined,
|
||||||
|
"Content-Type": data ? "application/json" : undefined,
|
||||||
|
},
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserToken(username) {
|
||||||
|
if(username in this.tokens) {
|
||||||
|
return this.tokens[username];
|
||||||
|
}
|
||||||
|
|
||||||
|
let req = await this.api("/api/auth/register", "POST", {
|
||||||
|
firstname: username,
|
||||||
|
lastname: username === this.bridge_username ? "[bridge]" : "[matrix]",
|
||||||
|
email: username,
|
||||||
|
password: username,
|
||||||
|
gender: username === this.bridge_username ? "matrix bridge" : "matrix user",
|
||||||
|
day: 1,
|
||||||
|
month: 1,
|
||||||
|
year: 1970,
|
||||||
|
});
|
||||||
|
if(req.ok) {
|
||||||
|
const token = decodeURIComponent(/auth=([^;]+);/.exec(req.headers.get("set-cookie"))[1]);
|
||||||
|
return this.tokens[username] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("couldn't register " + username + ":", await req.text());
|
||||||
|
|
||||||
|
req = await this.api("/api/auth/login", "POST", {
|
||||||
|
email: username,
|
||||||
|
password: username,
|
||||||
|
});
|
||||||
|
if(req.ok) {
|
||||||
|
const token = decodeURIComponent(/auth=([^;]+);/.exec(req.headers.get("set-cookie"))[1]);
|
||||||
|
return this.tokens[username] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("couldn't login " + username + ":", await req.text());
|
||||||
|
|
||||||
|
throw new Error("Couldn't get token for " + username);
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiUser(url, method, data, username) {
|
||||||
|
const token = await this.getUserToken(username);
|
||||||
|
return await this.api(url, method, data, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsers(ids) {
|
||||||
|
const req = await this.apiUser("/api/users/load", "POST", {
|
||||||
|
ids,
|
||||||
|
}, this.bridge_username);
|
||||||
|
|
||||||
|
return await req.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(username, content) {
|
||||||
|
const res = await this.apiUser("/api/posts/create", "POST", {
|
||||||
|
content,
|
||||||
|
}, username);
|
||||||
|
return (await res.json()).post_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async pollLoop() {
|
||||||
|
let latest_post_id = 0, first = true;
|
||||||
|
while(true) {
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
const req = await this.apiUser("/api/posts/page", "POST", {
|
||||||
|
page: 0,
|
||||||
|
}, this.bridge_username);
|
||||||
|
const posts = await req.json();
|
||||||
|
|
||||||
|
if(!Array.isArray(posts) || !posts.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_posts = posts.filter(n => n.post_id > latest_post_id);
|
||||||
|
if(!new_posts.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_post_id = Math.max(latest_post_id, ...new_posts.map(n => n.post_id));
|
||||||
|
|
||||||
|
if(first) { // ignore posts from the first sync
|
||||||
|
first = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users_list = await this.getUsers(new_posts.map(n => n.user_id));
|
||||||
|
const users = {};
|
||||||
|
users_list.forEach(user => {
|
||||||
|
users[user.user_id] = user;
|
||||||
|
});
|
||||||
|
|
||||||
|
new_posts.forEach(post => {
|
||||||
|
const user = users[post.user_id];
|
||||||
|
|
||||||
|
this.emit("post", post, user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue