Back stickerpacks with rooms and use appservices to do so

This commit is contained in:
Travis Ralston 2019-03-17 22:18:28 -06:00
parent bac85a44a2
commit 910d4f3b56
15 changed files with 745 additions and 1698 deletions

View file

@ -1,9 +1,9 @@
# matrix-sticker-bot
# matrix-sticker-manager
[![#stickerbot:t2bot.io](https://img.shields.io/badge/matrix-%23stickerbot:t2bot.io-brightgreen.svg)](https://matrix.to/#/#stickerbot:t2bot.io)
[![TravisCI badge](https://travis-ci.org/turt2live/matrix-sticker-bot.svg?branch=master)](https://travis-ci.org/turt2live/matrix-sticker-bot)
[![#stickermanager:t2bot.io](https://img.shields.io/badge/matrix-%23stickermanager:t2bot.io-brightgreen.svg)](https://matrix.to/#/#stickermanager:t2bot.io)
[![TravisCI badge](https://travis-ci.org/turt2live/matrix-sticker-bot.svg?branch=master)](https://travis-ci.org/turt2live/matrix-sticker-manager)
A matrix bot to allow users to make their own sticker packs.
A matrix service to allow users to make their own sticker packs.
# Usage
@ -19,3 +19,18 @@ A matrix bot to allow users to make their own sticker packs.
3. `npm run build`
4. Copy `config/default.yaml` to `config/production.yaml`
5. Run the bot with `NODE_ENV=production node index.js`
You will also need to create an appservice registration file and add it to your homeserver:
```yaml
id: sticker-manager
url: http://localhost:8082
as_token: "YOUR_AS_TOKEN"
hs_token: "YOUR_HS_TOKEN"
sender_localpart: "stickers"
namespaces:
users: []
rooms: []
aliases:
- exclusive: true
regex: "#_stickerpack_.+:t2bot.io"
```

View file

@ -1,5 +1,9 @@
homeserverUrl: "https://t2bot.io"
accessToken: "YOUR_TOKEN_HERE"
# The application service settings
appservice:
domainName: "t2bot.io"
homeserverUrl: "https://t2bot.io"
asToken: "SECRET_TOKEN_HERE"
hsToken: "DIFFERENT_TOKEN_HERE"
# The webserver settings
webserver:
@ -19,17 +23,5 @@ media:
# Requires turt2live/matrix-media-repo
useMediaInfo: false
# The database options
database:
# The relative path to the sqlite database
file: "stickerbot.db"
# Settings for controlling how logging works
logging:
file: logs/stickerbot.log
console: true
consoleLevel: info
fileLevel: verbose
rotate:
size: 52428800 # bytes, default is 50mb
count: 5
# Where to store misc files
dataPath: "/etc/stickerbot/data"

2059
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,34 +1,25 @@
{
"name": "matrix-sticker-bot",
"name": "matrix-sticker-manager",
"version": "1.0.0",
"description": "A matrix bot to allow user-generated sticker packs to be created",
"description": "A matrix service to allow user-generated sticker packs to be created",
"main": "./lib/index.js",
"author": "Travis Ralston",
"license": "GPL-3.0",
"repository": {
"type": "git",
"url": "git+https://github.com/turt2live/matrix-sticker-bot.git"
"url": "git+https://github.com/turt2live/matrix-sticker-manager.git"
},
"dependencies": {
"@types/node": "^9.4.6",
"config": "^1.29.4",
"express": "^4.16.3",
"js-yaml": "^3.10.0",
"matrix-bot-sdk": "^0.1.8",
"matrix-js-snippets": "^0.2.5",
"mkdirp": "^0.5.1",
"node-localstorage": "^1.3.0",
"@types/node": "^10.14.1",
"config": "^3.0.1",
"js-yaml": "^3.12.2",
"matrix-bot-sdk": "^0.3.3",
"random-string": "^0.2.0",
"reflect-metadata": "^0.1.12",
"sequelize": "^4.37.6",
"sequelize-typescript": "^0.6.3",
"sqlite": "^2.9.1",
"striptags": "^3.1.1",
"umzug": "^2.1.0"
"striptags": "^3.1.1"
},
"devDependencies": {
"tslint": "^5.9.1",
"typescript": "^2.7.2"
"tslint": "^5.14.0",
"typescript": "^3.3.3333"
},
"scripts": {
"build": "tsc",

View file

@ -1,29 +0,0 @@
import { IFilterInfo, IStorageProvider } from "matrix-bot-sdk";
import { LocalStorage } from "node-localstorage";
import * as mkdirp from "mkdirp";
export class LocalstorageStorageProvider implements IStorageProvider {
private kvStore: Storage;
constructor(path: string) {
mkdirp.sync(path);
this.kvStore = new LocalStorage(path, 100 * 1024 * 1024); // quota is 100mb
}
setSyncToken(token: string | null): void {
this.kvStore.setItem("sync_token", token);
}
getSyncToken(): string | null {
return this.kvStore.getItem("sync_token");
}
setFilter(filter: IFilterInfo): void {
// Do nothing
}
getFilter(): IFilterInfo {
return null;
}
}

View file

@ -1,24 +1,19 @@
import { MatrixClient } from "matrix-bot-sdk";
import { LogService, MatrixClient } from "matrix-bot-sdk";
import { StickerPackBuilder } from "./builder";
import config from "../config";
import { LogService } from "matrix-js-snippets";
export interface Sticker {
description: string;
contentUri: string;
}
import { StickerMetadata } from "../storage/StickerStore";
export class GatherStickersStage implements StickerPackBuilder {
public stickers: Sticker[] = [];
private currentSticker: Sticker = {description: "", contentUri: ""};
public stickers: StickerMetadata[] = [];
private currentSticker: StickerMetadata = {description: "", contentUri: ""};
private expectingImage = true;
private resolveFn: (stickers: Sticker[]) => void;
private resolveFn: (stickers: StickerMetadata[]) => void;
constructor(private client: MatrixClient, private roomId: string) {
}
public start(): Promise<Sticker[]> {
public start(): Promise<StickerMetadata[]> {
return new Promise((resolve, _reject) => {
this.resolveFn = resolve;
});

View file

@ -1,10 +1,9 @@
import { StickerPackBuilder } from "./builder";
import { MatrixClient } from "matrix-bot-sdk";
import { GatherStickersStage, Sticker } from "./GatherStickersStage";
import { LogService, MatrixClient } from "matrix-bot-sdk";
import { GatherStickersStage } from "./GatherStickersStage";
import * as randomString from "random-string";
import Stickerpack from "../db/models/Stickerpack";
import StickerRecord from "../db/models/StickerRecord";
import config from "../config";
import { StickerMetadata, StickerStore } from "../storage/StickerStore";
export class NewPackBuilder implements StickerPackBuilder {
@ -13,7 +12,7 @@ export class NewPackBuilder implements StickerPackBuilder {
private expectingName = true;
private gatherStage: GatherStickersStage;
constructor(private client: MatrixClient, private roomId: string) {
constructor(private client: MatrixClient, private roomId: string, private store: StickerStore) {
client.sendNotice(roomId, "Woot! A new sticker pack. What should we call it?");
this.gatherStage = new GatherStickersStage(client, roomId);
}
@ -33,24 +32,23 @@ export class NewPackBuilder implements StickerPackBuilder {
}
}
private async createStickerPack(stickers: Sticker[]): Promise<any> {
private async createStickerPack(stickers: StickerMetadata[]): Promise<any> {
const members = await this.client.getJoinedRoomMembers(this.roomId);
const selfId = await this.client.getUserId();
const creatorId = members.filter(m => m !== selfId)[0];
if (!creatorId) throw new Error("Could not find a user ID to own this sticker pack");
const packId = (randomString({length: 10}) + "-" + this.name.replace(/[^a-zA-Z0-9-]/g, '-')).substring(0, 30);
const pack = await Stickerpack.create({id: packId, creatorId: creatorId, name: this.name});
for (const sticker of stickers) {
await StickerRecord.create({
packId: packId,
description: sticker.description,
contentUri: sticker.contentUri,
});
}
const pack = await this.store.createStickerpack({
id: packId,
name: this.name,
creatorId: creatorId,
stickers: stickers,
});
LogService.info("NewPackBuilder", `Pack for ${creatorId} created in room ${pack.roomId}`);
const slug = `${creatorId}/${packId}`;
const slug = `pack/${creatorId}/${packId}`;
const baseUrl = config.webserver.publicUrl;
const url = (baseUrl.endsWith("/") ? baseUrl : baseUrl + "/") + slug;
return this.client.sendNotice(this.roomId, "Awesome! I've created your sticker pack and published it here: " + url);

View file

@ -1,9 +1,12 @@
import * as config from "config";
import { LogConfig } from "matrix-js-snippets";
interface IConfig {
homeserverUrl: string;
accessToken: string;
appservice: {
domainName: string;
homeserverUrl: string;
asToken: string;
hsToken: string;
};
webserver: {
port: number;
bind: string;
@ -13,10 +16,7 @@ interface IConfig {
useLocalCopy: boolean;
useMediaInfo: boolean;
};
database: {
file: string;
};
logging: LogConfig;
dataPath: string;
}
export default <IConfig>config;

View file

@ -1,43 +0,0 @@
import { Model, Sequelize } from "sequelize-typescript";
import { LogService } from "matrix-js-snippets";
import config from "../config";
import * as path from "path";
import * as Umzug from "umzug";
import Stickerpack from "./models/Stickerpack";
import Sticker from "./models/StickerRecord";
class _StickerStore {
private sequelize: Sequelize;
constructor() {
this.sequelize = new Sequelize({
dialect: 'sqlite',
database: "stickerbot",
storage: config.database.file,
username: "",
password: "",
logging: i => LogService.verbose("StickerStore [SQL]", i)
});
this.sequelize.addModels(<Array<typeof Model>>[
Sticker,
Stickerpack,
]);
}
public updateSchema(): Promise<any> {
LogService.info("StickerStore", "Updating schema...");
const migrator = new Umzug({
storage: "sequelize",
storageOptions: {sequelize: this.sequelize},
migrations: {
params: [this.sequelize.getQueryInterface()],
path: path.join(__dirname, "migrations"),
},
});
return migrator.up();
}
}
export const StickerStore = new _StickerStore();

View file

@ -1,29 +0,0 @@
import { QueryInterface } from "sequelize";
import { DataType } from "sequelize-typescript";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.createTable("stickerpacks", {
"id": {type: DataType.STRING, primaryKey: true, allowNull: false},
"creatorId": {type: DataType.STRING, allowNull: false},
"name": {type: DataType.STRING, allowNull: false},
}))
.then(() => queryInterface.createTable("stickers", {
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
"packId": {
type: DataType.STRING,
allowNull: false,
references: {model: "stickerpacks", key: "id"},
onUpdate: "cascade", onDelete: "cascade",
},
"description": {type: DataType.STRING, allowNull: false},
"contentUri": {type: DataType.STRING, allowNull: false},
}));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.dropTable("stickers"))
.then(() => queryInterface.dropTable("stickerpacks"));
}
}

View file

@ -1,23 +0,0 @@
import { Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
import Stickerpack from "./Stickerpack";
@Table({
tableName: "stickers",
underscoredAll: false,
timestamps: false,
})
export default class StickerRecord extends Model<StickerRecord> {
@PrimaryKey
@Column
id: number;
@Column
@ForeignKey(() => Stickerpack)
packId: string;
@Column
description: string;
@Column
contentUri: string;
}

View file

@ -1,18 +0,0 @@
import { Column, Model, PrimaryKey, Table } from "sequelize-typescript";
@Table({
tableName: "stickerpacks",
underscoredAll: false,
timestamps: false,
})
export default class Stickerpack extends Model<Stickerpack> {
@PrimaryKey
@Column
id: string;
@Column
creatorId: string;
@Column
name: string;
}

View file

@ -1,24 +1,47 @@
import { AutojoinRoomsMixin, MatrixClient, SimpleRetryJoinStrategy } from "matrix-bot-sdk";
import config from "./config";
import { LogService } from "matrix-js-snippets";
import { CommandProcessor } from "./matrix/CommandProcessor";
import { LocalstorageStorageProvider } from "./bot/LocalstorageStorageProvider";
import { Appservice } from "matrix-bot-sdk/lib/appservice/Appservice";
import { SimpleFsStorageProvider } from "matrix-bot-sdk/lib/storage/SimpleFsStorageProvider";
import { BuilderRegistry } from "./bot/BuilderRegistry";
import { StickerStore } from "./db/StickerStore";
import { Webserver } from "./web/webserver";
LogService.configure(config.logging);
const storageProvider = new LocalstorageStorageProvider("./storage");
const client = new MatrixClient(config.homeserverUrl, config.accessToken, storageProvider);
const commands = new CommandProcessor(client);
import { LogService } from "matrix-bot-sdk/lib/logging/LogService";
import Webserver from "./web/webserver";
import * as path from "path";
import { StickerStore } from "./storage/StickerStore";
async function run() {
await StickerStore.updateSchema();
Webserver.start();
// Cheat and use a client to get the user ID for the appservice bot
const client = new MatrixClient(config.appservice.homeserverUrl, config.appservice.asToken);
const userId = await client.getUserId();
const localpart = userId.substring(1).split(":")[0];
client.on("room.message", (roomId, event) => {
const store = new StickerStore(client);
const commands = new CommandProcessor(client, store);
const appservice = new Appservice({
bindAddress: config.webserver.bind,
port: config.webserver.port,
homeserverName: config.appservice.domainName,
homeserverUrl: config.appservice.homeserverUrl,
storage: new SimpleFsStorageProvider(path.join(config.dataPath, "bot.json")),
registration: {
as_token: config.appservice.asToken,
hs_token: config.appservice.hsToken,
id: "stickers",
namespaces: {
users: [{regex: "@stickers.*", exclusive: true}],
aliases: [],
rooms: [],
},
sender_localpart: localpart,
url: "http://localhost",
},
joinStrategy: new SimpleRetryJoinStrategy(),
});
Webserver.begin(appservice, store);
appservice.on("room.message", (roomId, event) => {
if (event['sender'] === userId) return;
if (!event['content']) return;
if (event['type'] !== "m.room.message" && event['type'] !== "m.sticker") return; // Everything we care about is a message or sticker
@ -33,9 +56,8 @@ async function run() {
return commands.tryCommand(roomId, event);
});
AutojoinRoomsMixin.setupOnClient(client);
client.setJoinStrategy(new SimpleRetryJoinStrategy());
return client.start();
AutojoinRoomsMixin.setupOnAppservice(appservice);
return appservice.begin();
}
run().then(() => LogService.info("index", "Sticker bot started!"));

View file

@ -1,11 +1,11 @@
import { MatrixClient } from "matrix-bot-sdk";
import { LogService } from "matrix-js-snippets";
import { LogService, MatrixClient } from "matrix-bot-sdk";
import * as striptags from "striptags";
import { BuilderRegistry } from "../bot/BuilderRegistry";
import { NewPackBuilder } from "../builders/NewPackBuilder";
import { StickerStore } from "../storage/StickerStore";
export class CommandProcessor {
constructor(private client: MatrixClient) {
constructor(private client: MatrixClient, private store: StickerStore) {
}
public async tryCommand(roomId: string, event: any): Promise<any> {
@ -30,7 +30,7 @@ export class CommandProcessor {
return this.client.sendNotice(roomId, "Oops! It looks like you're already doing something. Please finish your current operation before creating a new sticker pack.");
}
BuilderRegistry.register(roomId, new NewPackBuilder(this.client, roomId)); // sends a welcome message
BuilderRegistry.register(roomId, new NewPackBuilder(this.client, roomId, this.store)); // sends a welcome message
} else if (command === "cancel") {
if (BuilderRegistry.hasBuilder(roomId)) {
BuilderRegistry.deregister(roomId);

View file

@ -1,24 +1,23 @@
import * as express from "express";
import config from "../config";
import { LogService } from "matrix-js-snippets";
import Stickerpack from "../db/models/Stickerpack";
import StickerRecord from "../db/models/StickerRecord";
import { Appservice, LogService, MatrixClient } from "matrix-bot-sdk";
import { StickerStore } from "../storage/StickerStore";
export default class Webserver {
private static instance: Webserver;
class _Webserver {
private app: any;
constructor() {
this.app = express();
constructor(private appservice: Appservice, private store: StickerStore) {
this.app = (<any>appservice).app; // HACK: Private variable access
this.app.get('/:userId/:packId', this.getStickerpack.bind(this));
this.app.get('/pack/:userId/:packId', this.getStickerpack.bind(this));
// TODO: Serve a home page of some kind. Stickerpack browser?
}
public start() {
this.app.listen(config.webserver.port, config.webserver.bind, () => {
LogService.info("webserver", "Listening on " + config.webserver.bind + ":" + config.webserver.port);
});
public static begin(appservice: Appservice, store: StickerStore) {
if (Webserver.instance) throw new Error("Already started");
Webserver.instance = new Webserver(appservice, store);
}
private async getStickerpack(req, res) {
@ -30,7 +29,7 @@ class _Webserver {
const replyJson = accept.indexOf("application/json") !== -1 || packId.endsWith(".json");
if (packId.endsWith(".json")) packId = packId.substring(0, packId.length - ".json".length);
const pack = await Stickerpack.findOne({where: {creatorId: userId, id: packId}});
const pack = await this.store.getStickerpack(packId);
if (!pack) {
// TODO: A real 404 page
res.status(404);
@ -38,7 +37,12 @@ class _Webserver {
return;
}
const stickers = await StickerRecord.findAll({where: {packId: pack.id}});
if (pack.creatorId !== userId) {
// TODO: A real 400 page
res.status(400);
res.send({"TODO": "A real 400 page"});
return;
}
if (replyJson) {
LogService.info("Webserver", "Serving JSON for pack " + pack.id);
@ -51,13 +55,18 @@ class _Webserver {
type: "mx-user",
id: pack.creatorId,
},
stickers: stickers.map(s => {
stickers: pack.stickers.map(s => {
return {
id: s.id, // arbitrary
description: s.description,
contentUri: s.contentUri,
};
}),
subscription: {
roomId: pack.roomId,
roomAlias: pack.roomAlias,
public: true,
},
});
return;
}
@ -65,6 +74,4 @@ class _Webserver {
// TODO: A real preview page
res.send({"TODO": "A real preview page"});
}
}
export const Webserver = new _Webserver();
}