Initial sticker pack creation
This commit is contained in:
parent
2d9d47bdad
commit
bac85a44a2
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,3 +1,13 @@
|
|||
config/*
|
||||
!config/default.yaml
|
||||
lib/
|
||||
storage/
|
||||
|
||||
/.idea
|
||||
|
||||
/db
|
||||
*.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
|
13
.travis.yml
Normal file
13
.travis.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "9"
|
||||
env:
|
||||
- NODE_ENV=development
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
install:
|
||||
- npm install
|
||||
script:
|
||||
- npm run build
|
||||
- npm run lint
|
||||
|
21
README.md
21
README.md
|
@ -1,2 +1,21 @@
|
|||
# matrix-sticker-bot
|
||||
A bot to help people create their own sticker packs
|
||||
|
||||
[![#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)
|
||||
|
||||
A matrix bot to allow users to make their own sticker packs.
|
||||
|
||||
# Usage
|
||||
|
||||
1. Invite `@stickers:t2bot.io` to a private chat
|
||||
2. Send the message `!stickers help` to get information about how to use the bot
|
||||
|
||||
# Building your own
|
||||
|
||||
*Note*: You'll need to have access to an account that the bot can use to get the access token.
|
||||
|
||||
1. Clone this repository
|
||||
2. `npm install`
|
||||
3. `npm run build`
|
||||
4. Copy `config/default.yaml` to `config/production.yaml`
|
||||
5. Run the bot with `NODE_ENV=production node index.js`
|
||||
|
|
35
config/default.yaml
Normal file
35
config/default.yaml
Normal file
|
@ -0,0 +1,35 @@
|
|||
homeserverUrl: "https://t2bot.io"
|
||||
accessToken: "YOUR_TOKEN_HERE"
|
||||
|
||||
# The webserver settings
|
||||
webserver:
|
||||
port: 8082
|
||||
bind: 0.0.0.0
|
||||
|
||||
# The public URL is used to provide links to users who generate sticker packs
|
||||
publicUrl: 'https://stickers.t2bot.io/'
|
||||
|
||||
# Media repository settings
|
||||
media:
|
||||
# Set to true to clone uploaded media on your media repository.
|
||||
# Requires turt2live/matrix-media-repo
|
||||
useLocalCopy: false
|
||||
|
||||
# Set to true to use the server-side media info to check the images.
|
||||
# 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
|
2162
package-lock.json
generated
Normal file
2162
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
package.json
Normal file
37
package.json
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "matrix-sticker-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "A matrix bot 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"
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tslint": "^5.9.1",
|
||||
"typescript": "^2.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "tslint --project ./tsconfig.json --type-check -t stylish"
|
||||
}
|
||||
}
|
29
src/bot/BuilderRegistry.ts
Normal file
29
src/bot/BuilderRegistry.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { StickerPackBuilder } from "../builders/builder";
|
||||
|
||||
class _BuilderRegistry {
|
||||
private roomIdToBuilder: { [roomId: string]: StickerPackBuilder } = {};
|
||||
|
||||
public register(roomId: string, builder: StickerPackBuilder): void {
|
||||
if (this.roomIdToBuilder[roomId]) {
|
||||
throw new Error("An operation is already in progress");
|
||||
}
|
||||
|
||||
this.roomIdToBuilder[roomId] = builder;
|
||||
}
|
||||
|
||||
public deregister(roomId: string): void {
|
||||
delete this.roomIdToBuilder[roomId];
|
||||
}
|
||||
|
||||
public handleEvent(roomId: string, event: any): Promise<any> {
|
||||
if (this.roomIdToBuilder[roomId]) {
|
||||
return this.roomIdToBuilder[roomId].handleEvent(event);
|
||||
} else return Promise.resolve();
|
||||
}
|
||||
|
||||
public hasBuilder(roomId: string): boolean {
|
||||
return !!this.roomIdToBuilder[roomId];
|
||||
}
|
||||
}
|
||||
|
||||
export const BuilderRegistry = new _BuilderRegistry();
|
29
src/bot/LocalstorageStorageProvider.ts
Normal file
29
src/bot/LocalstorageStorageProvider.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
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;
|
||||
}
|
||||
}
|
110
src/builders/GatherStickersStage.ts
Normal file
110
src/builders/GatherStickersStage.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { 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;
|
||||
}
|
||||
|
||||
export class GatherStickersStage implements StickerPackBuilder {
|
||||
|
||||
public stickers: Sticker[] = [];
|
||||
private currentSticker: Sticker = {description: "", contentUri: ""};
|
||||
private expectingImage = true;
|
||||
private resolveFn: (stickers: Sticker[]) => void;
|
||||
|
||||
constructor(private client: MatrixClient, private roomId: string) {
|
||||
}
|
||||
|
||||
public start(): Promise<Sticker[]> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
this.resolveFn = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
public async handleEvent(event: any): Promise<any> {
|
||||
if (event['type'] === "m.room.message" && event['content']['msgtype'] === "m.text") {
|
||||
if (event['content']['body'] === "!done") {
|
||||
LogService.info("GatherStickersStage", "Finished sticker gathering for " + this.roomId);
|
||||
this.resolveFn(this.stickers);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!this.expectingImage) {
|
||||
this.currentSticker.description = event['content']['body'];
|
||||
this.stickers.push(this.currentSticker);
|
||||
this.currentSticker = {description: "", contentUri: ""};
|
||||
this.expectingImage = true;
|
||||
LogService.info("GatherStickersStage", "A sticker has been completed, but not submitted in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "Thanks! Send me another 512x512 PNG for your next sticker or say !done if you've finished.");
|
||||
}
|
||||
}
|
||||
|
||||
if (event['type'] !== "m.room.message" || !event['content']['url'] || event['content']['msgtype'] !== "m.image") {
|
||||
LogService.warn("GatherStickersStage", "Event does not look to be an image event in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "That doesn't look like an image to me. Please send a 512x512 PNG of the sticker you'd like to add.");
|
||||
}
|
||||
|
||||
const mxc = event['content']['url'];
|
||||
if (!mxc.startsWith("mxc://")) {
|
||||
LogService.warn("GatherStickersStage", "Not an MXC URI in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "That doesn't look like a valid image, sorry.");
|
||||
}
|
||||
|
||||
const mxcParts = mxc.substring("mxc://".length).split("/");
|
||||
const origin = mxcParts[0];
|
||||
const mediaId = mxcParts[1];
|
||||
if (!origin || !mediaId) {
|
||||
LogService.warn("GatherStickersStage", "Invalid format for content URI in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "That doesn't look like a valid image, sorry.");
|
||||
}
|
||||
|
||||
if (config.media.useMediaInfo) {
|
||||
try {
|
||||
LogService.info("GatherStickersStage", "Requesting media info for " + mxc);
|
||||
const response = await this.client.doRequest("GET", "/_matrix/media/r0/info/" + origin + "/" + mediaId);
|
||||
if (response['content_type'] !== "image/png" || !response['width'] || !response['height']) {
|
||||
LogService.warn("GatherStickersStage", "Media info for " + mxc + " indicates the file is invalid in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "Please upload a PNG image for your sticker.");
|
||||
}
|
||||
} catch (err) {
|
||||
LogService.error("GatherStickersStage", "Error requesting media info:");
|
||||
LogService.error("GatherStickersStage", err);
|
||||
return this.client.sendNotice(this.roomId, "Something went wrong while checking your sticker. Please try again.");
|
||||
}
|
||||
} else {
|
||||
if (!event['content']['info']) {
|
||||
LogService.warn("GatherStickersStage", "Event is missing media info in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "Your client didn't send me enough information for me to validate your sticker. Please try again or use a different client.");
|
||||
}
|
||||
if (event['content']['info']['mimetype'] !== "image/png") {
|
||||
LogService.warn("GatherStickersStage", "Media info from event indicates the file is not an image in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "Please upload a PNG image for your sticker.");
|
||||
}
|
||||
}
|
||||
|
||||
let contentUri = "mxc://" + origin + "/" + mediaId;
|
||||
if (config.media.useLocalCopy) {
|
||||
try {
|
||||
LogService.info("GatherStickersStage", "Requesting local copy of " + contentUri);
|
||||
const response = await this.client.doRequest("GET", "/_matrix/media/r0/local_copy/" + origin + "/" + mediaId);
|
||||
contentUri = response["content_uri"];
|
||||
LogService.info("GatherStickersStage", "Local copy for " + mxc + " is " + contentUri);
|
||||
} catch (err) {
|
||||
LogService.error("GatherStickersStage", "Error getting local copy:");
|
||||
LogService.error("GatherStickersStage", err);
|
||||
return this.client.sendNotice(this.roomId, "Something went wrong with handling your sticker. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
this.currentSticker = {
|
||||
description: "",
|
||||
contentUri: contentUri,
|
||||
};
|
||||
this.expectingImage = false;
|
||||
LogService.info("GatherStickersStage", "Asking for a description for the uploaded image in " + this.roomId);
|
||||
return this.client.sendNotice(this.roomId, "Great! In a few words, please describe your sticker.");
|
||||
}
|
||||
}
|
58
src/builders/NewPackBuilder.ts
Normal file
58
src/builders/NewPackBuilder.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { StickerPackBuilder } from "./builder";
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { GatherStickersStage, Sticker } from "./GatherStickersStage";
|
||||
import * as randomString from "random-string";
|
||||
import Stickerpack from "../db/models/Stickerpack";
|
||||
import StickerRecord from "../db/models/StickerRecord";
|
||||
import config from "../config";
|
||||
|
||||
|
||||
export class NewPackBuilder implements StickerPackBuilder {
|
||||
|
||||
private name: string;
|
||||
private expectingName = true;
|
||||
private gatherStage: GatherStickersStage;
|
||||
|
||||
constructor(private client: MatrixClient, private roomId: string) {
|
||||
client.sendNotice(roomId, "Woot! A new sticker pack. What should we call it?");
|
||||
this.gatherStage = new GatherStickersStage(client, roomId);
|
||||
}
|
||||
|
||||
public async handleEvent(event: any): Promise<any> {
|
||||
if (this.expectingName) {
|
||||
if (event['type'] !== "m.room.message" || !event['content']['body'] || event['content']['msgtype'] !== "m.text") {
|
||||
return this.client.sendNotice(this.roomId, "Not quite the type of name I was expecting. Let's start with what we should call your new pack. To give it a name, just send me a message with your pack's name.");
|
||||
}
|
||||
|
||||
this.name = event['content']['body'];
|
||||
this.expectingName = false;
|
||||
this.gatherStage.start().then(stickers => this.createStickerPack(stickers));
|
||||
return this.client.sendNotice(this.roomId, "Thanks! Now send me your first sticker. The image should be a PNG image (with a transparent background) and should be 512x512.\n\nThe sticker should also have a white border around it.");
|
||||
} else {
|
||||
return this.gatherStage.handleEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private async createStickerPack(stickers: Sticker[]): 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 slug = `${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);
|
||||
}
|
||||
}
|
3
src/builders/builder.ts
Normal file
3
src/builders/builder.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export interface StickerPackBuilder {
|
||||
handleEvent(event: any): Promise<any>;
|
||||
}
|
22
src/config.ts
Normal file
22
src/config.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as config from "config";
|
||||
import { LogConfig } from "matrix-js-snippets";
|
||||
|
||||
interface IConfig {
|
||||
homeserverUrl: string;
|
||||
accessToken: string;
|
||||
webserver: {
|
||||
port: number;
|
||||
bind: string;
|
||||
publicUrl: string;
|
||||
};
|
||||
media: {
|
||||
useLocalCopy: boolean;
|
||||
useMediaInfo: boolean;
|
||||
};
|
||||
database: {
|
||||
file: string;
|
||||
};
|
||||
logging: LogConfig;
|
||||
}
|
||||
|
||||
export default <IConfig>config;
|
43
src/db/StickerStore.ts
Normal file
43
src/db/StickerStore.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
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();
|
29
src/db/migrations/20180407200945-AddTables.ts
Normal file
29
src/db/migrations/20180407200945-AddTables.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
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"));
|
||||
}
|
||||
}
|
23
src/db/models/StickerRecord.ts
Normal file
23
src/db/models/StickerRecord.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
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;
|
||||
}
|
18
src/db/models/Stickerpack.ts
Normal file
18
src/db/models/Stickerpack.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
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;
|
||||
}
|
41
src/index.ts
Normal file
41
src/index.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
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 { 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);
|
||||
|
||||
async function run() {
|
||||
await StickerStore.updateSchema();
|
||||
Webserver.start();
|
||||
|
||||
const userId = await client.getUserId();
|
||||
|
||||
client.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
|
||||
|
||||
const isText = event['content']['msgtype'] === "m.text";
|
||||
const isCommand = isText && (event['content']['body'] || "").toString().startsWith("!stickers");
|
||||
|
||||
if (BuilderRegistry.hasBuilder(roomId) && !isCommand) {
|
||||
return BuilderRegistry.handleEvent(roomId, event);
|
||||
}
|
||||
|
||||
return commands.tryCommand(roomId, event);
|
||||
});
|
||||
|
||||
AutojoinRoomsMixin.setupOnClient(client);
|
||||
client.setJoinStrategy(new SimpleRetryJoinStrategy());
|
||||
return client.start();
|
||||
}
|
||||
|
||||
run().then(() => LogService.info("index", "Sticker bot started!"));
|
54
src/matrix/CommandProcessor.ts
Normal file
54
src/matrix/CommandProcessor.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import * as striptags from "striptags";
|
||||
import { BuilderRegistry } from "../bot/BuilderRegistry";
|
||||
import { NewPackBuilder } from "../builders/NewPackBuilder";
|
||||
|
||||
export class CommandProcessor {
|
||||
constructor(private client: MatrixClient) {
|
||||
}
|
||||
|
||||
public async tryCommand(roomId: string, event: any): Promise<any> {
|
||||
const message = event['content']['body'];
|
||||
if (!message || !message.startsWith("!stickers")) return;
|
||||
|
||||
LogService.info("CommandProcessor", "Received command - checking room members");
|
||||
const members = await this.client.getJoinedRoomMembers(roomId);
|
||||
if (members.length > 2) {
|
||||
return this.client.sendNotice(roomId, "It is best to interact with me in a private room.");
|
||||
}
|
||||
|
||||
let command = "help";
|
||||
const args = message.substring("!stickers".length).trim().split(" ");
|
||||
if (args.length > 0) {
|
||||
command = args[0];
|
||||
args.splice(0, 1);
|
||||
}
|
||||
|
||||
if (command === "newpack") {
|
||||
if (BuilderRegistry.hasBuilder(roomId)) {
|
||||
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
|
||||
} else if (command === "cancel") {
|
||||
if (BuilderRegistry.hasBuilder(roomId)) {
|
||||
BuilderRegistry.deregister(roomId);
|
||||
return this.client.sendNotice(roomId, "Your current operation has been canceled");
|
||||
} else return this.client.sendNotice(roomId, "There's nothing for me to cancel");
|
||||
} else {
|
||||
const htmlMessage = "<p>Sticker bot help:<br /><pre><code>" +
|
||||
`!stickers newpack - Create a new sticker pack\n` +
|
||||
`!stickers cancel - Cancels whatever operation you're doing\n` +
|
||||
"!stickers help - This menu\n" +
|
||||
"</code></pre></p>" +
|
||||
"<p>For help or more information, visit <a href='https://matrix.to/#/#help:t2bot.io'>#help:t2bot.io</a></p>";
|
||||
return this.client.sendMessage(roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: striptags(htmlMessage),
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: htmlMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
70
src/web/webserver.ts
Normal file
70
src/web/webserver.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
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";
|
||||
|
||||
class _Webserver {
|
||||
private app: any;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
|
||||
this.app.get('/: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);
|
||||
});
|
||||
}
|
||||
|
||||
private async getStickerpack(req, res) {
|
||||
const accept = (req.headers['accept'] || "");
|
||||
const params = (req.params || {});
|
||||
const userId = params['userId'];
|
||||
let packId = params['packId'];
|
||||
|
||||
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}});
|
||||
if (!pack) {
|
||||
// TODO: A real 404 page
|
||||
res.status(404);
|
||||
res.send({"TODO": "A real 404 page"});
|
||||
return;
|
||||
}
|
||||
|
||||
const stickers = await StickerRecord.findAll({where: {packId: pack.id}});
|
||||
|
||||
if (replyJson) {
|
||||
LogService.info("Webserver", "Serving JSON for pack " + pack.id);
|
||||
res.send({
|
||||
version: 1,
|
||||
type: "m.stickerpack",
|
||||
name: pack.name,
|
||||
id: pack.id, // arbitrary
|
||||
author: {
|
||||
type: "mx-user",
|
||||
id: pack.creatorId,
|
||||
},
|
||||
stickers: stickers.map(s => {
|
||||
return {
|
||||
id: s.id, // arbitrary
|
||||
description: s.description,
|
||||
contentUri: s.contentUri,
|
||||
};
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: A real preview page
|
||||
res.send({"TODO": "A real preview page"});
|
||||
}
|
||||
}
|
||||
|
||||
export const Webserver = new _Webserver();
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2015",
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "./lib",
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
72
tslint.json
Normal file
72
tslint.json
Normal file
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"comment-format": [
|
||||
true
|
||||
],
|
||||
"curly": false,
|
||||
"eofline": false,
|
||||
"forin": false,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"label-position": true,
|
||||
"max-line-length": false,
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
"static-after-instance",
|
||||
"variables-before-functions"
|
||||
],
|
||||
"no-arg": true,
|
||||
"no-bitwise": false,
|
||||
"no-console": false,
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-empty": false,
|
||||
"no-eval": true,
|
||||
"no-inferrable-types": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-string-literal": false,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": false,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": false,
|
||||
"radix": true,
|
||||
"semicolon": [
|
||||
"always"
|
||||
],
|
||||
"triple-equals": [],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue