Support custom stickerpacks

This commit is contained in:
Travis Ralston 2019-03-20 22:32:29 -06:00
parent 968fb18a57
commit 147ef2104e
11 changed files with 244 additions and 25 deletions

30
package-lock.json generated
View File

@ -5659,7 +5659,8 @@
"ip-regex": { "ip-regex": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
"dev": true
}, },
"ipaddr.js": { "ipaddr.js": {
"version": "1.8.0", "version": "1.8.0",
@ -11078,22 +11079,22 @@
} }
}, },
"request-promise": { "request-promise": {
"version": "4.2.2", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz",
"integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", "integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==",
"requires": { "requires": {
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"request-promise-core": "1.1.1", "request-promise-core": "1.1.2",
"stealthy-require": "^1.1.0", "stealthy-require": "^1.1.1",
"tough-cookie": ">=2.3.3" "tough-cookie": "^2.3.3"
} }
}, },
"request-promise-core": { "request-promise-core": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
"integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==",
"requires": { "requires": {
"lodash": "^4.13.1" "lodash": "^4.17.11"
} }
}, },
"require-dir-all": { "require-dir-all": {
@ -12978,11 +12979,10 @@
"integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg="
}, },
"tough-cookie": { "tough-cookie": {
"version": "3.0.1", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"requires": { "requires": {
"ip-regex": "^2.1.0",
"psl": "^1.1.28", "psl": "^1.1.28",
"punycode": "^2.1.1" "punycode": "^2.1.1"
}, },

View File

@ -38,7 +38,7 @@
"netmask": "^1.0.6", "netmask": "^1.0.6",
"random-string": "^0.2.0", "random-string": "^0.2.0",
"request": "^2.88.0", "request": "^2.88.0",
"request-promise": "^4.2.2", "request-promise": "^4.2.4",
"require-dir-all": "^0.4.15", "require-dir-all": "^0.4.15",
"sequelize": "^4.39.1", "sequelize": "^4.39.1",
"sequelize-typescript": "^0.6.6", "sequelize-typescript": "^0.6.6",

View File

@ -5,6 +5,8 @@ import Sticker from "../../db/models/Sticker";
import { ScalarService } from "../scalar/ScalarService"; import { ScalarService } from "../scalar/ScalarService";
import UserStickerPack from "../../db/models/UserStickerPack"; import UserStickerPack from "../../db/models/UserStickerPack";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader";
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
export interface MemoryStickerPack { export interface MemoryStickerPack {
id: number; id: number;
@ -35,6 +37,7 @@ export interface MemoryStickerPack {
height: number; height: number;
}; };
}[]; }[];
trackingRoomAlias: string;
} }
export interface MemoryUserStickerPack extends MemoryStickerPack { export interface MemoryUserStickerPack extends MemoryStickerPack {
@ -45,6 +48,10 @@ interface SetSelectedRequest {
isSelected: boolean; isSelected: boolean;
} }
interface ImportPackRequest {
packUrl: string;
}
/** /**
* API for stickers * API for stickers
*/ */
@ -88,6 +95,7 @@ export class DimensionStickerService {
const selectedPack = JSON.parse(JSON.stringify(pack)); const selectedPack = JSON.parse(JSON.stringify(pack));
selectedPack.isSelected = userPack ? userPack.isSelected : false; selectedPack.isSelected = userPack ? userPack.isSelected : false;
if (!selectedPack.isSelected && pack.trackingRoomAlias) continue;
packs.push(selectedPack); packs.push(selectedPack);
} }
@ -119,6 +127,28 @@ export class DimensionStickerService {
return {}; // 200 OK return {}; // 200 OK
} }
@POST
@Path("packs/import")
public async importPack(@QueryParam("scalar_token") scalarToken: string, request: ImportPackRequest): Promise<MemoryUserStickerPack> {
await ScalarService.getTokenOwner(scalarToken);
const packUrl = request.packUrl.endsWith(".json") ? request.packUrl : `${request.packUrl}.json`;
const metadata = await StickerpackMetadataDownloader.getMetadata(packUrl);
await MatrixStickerBot.trackStickerpack(metadata.roomAlias);
const stickerPacks = await StickerPack.findAll({where: {trackingRoomAlias: metadata.roomAlias}});
Cache.for(CACHE_STICKERS).clear();
if (stickerPacks.length <= 0) throw new ApiError(500, "Stickerpack not imported");
const pack = stickerPacks[0];
// Simulate a call to setPackSelected
await this.setPackSelected(scalarToken, pack.id, {isSelected: true});
const memoryPack = await DimensionStickerService.packToMemory(pack);
return Object.assign({isSelected: true}, memoryPack);
}
public static async packToMemory(pack: StickerPack): Promise<MemoryStickerPack> { public static async packToMemory(pack: StickerPack): Promise<MemoryStickerPack> {
const stickers = await Sticker.findAll({where: {packId: pack.id}}); const stickers = await Sticker.findAll({where: {packId: pack.id}});
return { return {
@ -152,6 +182,7 @@ export class DimensionStickerService {
}, },
} }
}), }),
trackingRoomAlias: pack.trackingRoomAlias,
}; };
} }

View File

@ -0,0 +1,16 @@
import { QueryInterface } from "sequelize";
import { DataType } from "sequelize-typescript";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.addColumn("dimension_sticker_packs", "trackingRoomAlias", {
type: DataType.STRING,
allowNull: true
}));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.removeColumn("dimension_sticker_packs", "trackingRoomAlias"));
}
}

View File

@ -1,4 +1,4 @@
import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript"; import { AllowNull, AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript";
import { IntegrationRecord } from "./IntegrationRecord"; import { IntegrationRecord } from "./IntegrationRecord";
@Table({ @Table({
@ -44,4 +44,8 @@ export default class StickerPack extends Model<StickerPack> implements Integrati
@Column @Column
licensePath: string; licensePath: string;
@AllowNull
@Column
trackingRoomAlias: string;
} }

View File

@ -6,10 +6,14 @@ import {
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import config from "../config"; import config from "../config";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import StickerPack from "../db/models/StickerPack";
import Sticker from "../db/models/Sticker";
import { MatrixLiteClient } from "./MatrixLiteClient";
import { Cache, CACHE_STICKERS } from "../MemoryCache";
class _MatrixStickerBot { class _MatrixStickerBot {
private client: MatrixClient; private readonly client: MatrixClient;
constructor() { constructor() {
this.client = new MatrixClient( this.client = new MatrixClient(
@ -19,7 +23,6 @@ class _MatrixStickerBot {
this.client.setJoinStrategy(new SimpleRetryJoinStrategy()); this.client.setJoinStrategy(new SimpleRetryJoinStrategy());
this.client.on("room.event", this.onEvent.bind(this)); this.client.on("room.event", this.onEvent.bind(this));
this.client.on("room.upgraded", this.onUpgraded.bind(this));
AutojoinUpgradedRoomsMixin.setupOnClient(this.client); AutojoinUpgradedRoomsMixin.setupOnClient(this.client);
} }
@ -31,12 +34,109 @@ class _MatrixStickerBot {
return this.client.getUserId(); return this.client.getUserId();
} }
private onEvent(roomId, event) { private async onEvent(roomId, event) {
LogService.info("MatrixStickerBot", `Event ${event.type} in ${roomId}`); LogService.info("MatrixStickerBot", `Event ${event.type} in ${roomId}`);
if (event.type !== "io.t2bot.stickers.metadata" || event.state_key !== "") return;
const canonicalAlias = await this.client.getRoomStateEvent(roomId, "m.room.canonical_alias", "");
const stickerPacks = await StickerPack.findAll({where: {trackingRoomAlias: canonicalAlias.alias}});
if (stickerPacks.length > 0) {
return this.updateStickersInPacks(stickerPacks, roomId);
}
} }
private onUpgraded(roomId, event) { public trackStickerpack(alias: string): Promise<any> {
LogService.info("MatrixStickerBot", `Room ${roomId} upgraded due to ${event.type}`); return this.client.joinRoom(alias).then(async (roomId) => {
const stickerPacks = await StickerPack.findAll({where: {trackingRoomAlias: alias}});
if (stickerPacks.length > 0) {
return this.updateStickersInPacks(stickerPacks, roomId);
} else {
const pack = await StickerPack.create({
type: "stickerpack",
name: "PLACEHOLDER",
description: "PLACEHOLDER",
avatarUrl: "mxc://localhost/NotYetLoaded",
isEnabled: false,
isPublic: true,
authorType: "matrix",
authorName: await this.getUserId(),
authorReference: "https://matrix.to/#/" + (await this.getUserId()),
license: "Imported",
licensePath: "/licenses/general-imported.txt",
trackingRoomAlias: alias,
});
return this.updateStickersInPacks([pack], roomId);
}
});
}
private async updateStickersInPacks(stickerPacks: StickerPack[], roomId: string) {
const nameEvent = await this.client.getRoomStateEvent(roomId, "m.room.name", "");
if (!nameEvent) return null;
const canconicalAliasEvent = await this.client.getRoomStateEvent(roomId, "m.room.canonical_alias", "");
if (!canconicalAliasEvent) return null;
const packEvent = await this.client.getRoomStateEvent(roomId, "io.t2bot.stickers.metadata", "");
if (!packEvent) return null;
let authorDisplayName = packEvent.creatorId;
try {
const profile = await this.client.getUserProfile(packEvent.creatorId);
if (profile && profile.displayname) authorDisplayName = profile.displayname;
} catch (e) {
LogService.warn("MatrixStickerBot", e);
}
const mx = new MatrixLiteClient(config.homeserver.accessToken);
const stickerEvents = [];
for (const stickerId of packEvent.activeStickers) {
const stickerEvent = await this.client.getRoomStateEvent(roomId, "io.t2bot.stickers.sticker", stickerId);
if (!stickerEvent) continue;
const mxc = stickerEvent.contentUri;
const serverName = mxc.substring("mxc://".length).split("/")[0];
const contentId = mxc.substring("mxc://".length).split("/")[1];
stickerEvent.thumbMxc = await mx.uploadFromUrl(await mx.getThumbnailUrl(serverName, contentId, 512, 512, "scale", false), "image/png");
stickerEvents.push(stickerEvent);
}
for (const pack of stickerPacks) {
pack.isEnabled = true;
pack.authorType = "matrix";
pack.authorReference = "https://matrix.to/#/" + packEvent.creatorId;
pack.authorName = authorDisplayName;
pack.trackingRoomAlias = canconicalAliasEvent.alias;
pack.name = nameEvent.name;
pack.description = "Matrix sticker pack created by " + authorDisplayName;
pack.license = "Imported";
pack.licensePath = "/licenses/general-imported.txt";
if (stickerEvents.length > 0) pack.avatarUrl = stickerEvents[0].contentUri;
await pack.save();
const existingStickers = await Sticker.findAll({where: {packId: pack.id}});
for (const sticker of existingStickers) await sticker.destroy();
for (const stickerEvent of stickerEvents) {
await Sticker.create({
packId: pack.id,
name: stickerEvent.description,
description: stickerEvent.description,
imageMxc: stickerEvent.contentUri,
thumbnailMxc: stickerEvent.thumbMxc,
thumbnailWidth: 512,
thumbnailHeight: 512,
mimetype: "image/png",
});
}
}
LogService.info("MatrixStickerBot", `Updated ${stickerPacks.length} stickerpacks`);
Cache.for(CACHE_STICKERS).clear();
} }
} }

View File

@ -0,0 +1,25 @@
import * as request from "request-promise";
export interface StickerpackMetadata {
creatorId: string;
roomId: string;
roomAlias: string;
}
export class StickerpackMetadataDownloader {
public static getMetadata(packUrl: string): Promise<StickerpackMetadata> {
return request({
uri: packUrl,
headers: {
'Accept': 'application/json',
},
json: true,
}).then(body => {
if (!body || !body["io.t2bot.dimension"]) {
throw new Error("Failed to locate Dimension metadata");
}
return body["io.t2bot.dimension"];
});
}
}

View File

@ -2,11 +2,30 @@
<my-spinner></my-spinner> <my-spinner></my-spinner>
</div> </div>
<div *ngIf="!isLoading"> <div *ngIf="!isLoading">
<my-ibox title="Sticker Packs"> <my-ibox title="Sticker Packs" *ngIf="packs.length <= 0">
<div class="my-ibox-content" *ngIf="packs.length <= 0"> <div class="my-ibox-content">
<h5 style="text-align: center;">Sticker packs are not enabled on this Dimension instance.</h5> <h5 style="text-align: center;">Sticker packs are not enabled on this Dimension instance.</h5>
</div> </div>
<div class="my-ibox-content" *ngIf="packs.length > 0"> </my-ibox>
<my-ibox title="Add Sticker Packs" *ngIf="packs.length > 0">
<div class="my-ibox-content">
<form (submit)="importPack()" novalidate name="importForm">
<label class="label-block">
Stickerpack URL
<input type="text" class="form-control" name="packUrl"
placeholder="https://packs.t2bot.io/pack/..."
[(ngModel)]="packUrl" [disabled]="isImporting"/>
</label>
<div style="margin-top: 25px">
<button type="submit" class="btn btn-sm btn-success" [disabled]="isImporting">
<i class="fa fa-plus"></i> Add stickerpack
</button>
</div>
</form>
</div>
</my-ibox>
<my-ibox title="Sticker Packs" *ngIf="packs.length > 0">
<div class="my-ibox-content">
<div class="pack" *ngFor="let pack of packs trackById"> <div class="pack" *ngFor="let pack of packs trackById">
<img [src]="getThumbnailUrl(pack.avatarUrl, 120, 120)" width="120" height="120"/> <img [src]="getThumbnailUrl(pack.avatarUrl, 120, 120)" width="120" height="120"/>
<div class="caption"> <div class="caption">

View File

@ -16,6 +16,10 @@ export class StickerpickerComponent implements OnInit {
public isUpdating = false; public isUpdating = false;
public packs: FE_UserStickerPack[]; public packs: FE_UserStickerPack[];
// Import stuff
public packUrl = "";
public isImporting = false;
constructor(private stickerApi: StickerApiService, constructor(private stickerApi: StickerApiService,
private media: MediaService, private media: MediaService,
private scalarClient: ScalarClientApiService, private scalarClient: ScalarClientApiService,
@ -35,6 +39,21 @@ export class StickerpickerComponent implements OnInit {
} }
} }
public importPack() {
this.isImporting = true;
this.stickerApi.importStickerpack(this.packUrl).then(pack => {
// Insert at top for visibility
this.packs.splice(0, 0, pack);
this.packUrl = "";
this.isImporting = false;
this.toaster.pop("success", "Stickerpack added");
}).catch(err => {
console.error(err);
this.isImporting = false;
this.toaster.pop("error", "Error adding stickerpack");
});
}
public getThumbnailUrl(mxc: string, width: number, height: number, method: "crop" | "scale" = "scale"): string { public getThumbnailUrl(mxc: string, width: number, height: number, method: "crop" | "scale" = "scale"): string {
return this.media.getThumbnailUrl(mxc, width, height, method, true); return this.media.getThumbnailUrl(mxc, width, height, method, true);
} }

View File

@ -16,4 +16,8 @@ export class StickerApiService extends AuthedApi {
public togglePackSelection(packId: number, isSelected: boolean): Promise<any> { public togglePackSelection(packId: number, isSelected: boolean): Promise<any> {
return this.authedPost("/api/v1/dimension/stickers/packs/" + packId + "/selected", {isSelected: isSelected}).map(r => r.json()).toPromise(); return this.authedPost("/api/v1/dimension/stickers/packs/" + packId + "/selected", {isSelected: isSelected}).map(r => r.json()).toPromise();
} }
public importStickerpack(packUrl: string): Promise<FE_UserStickerPack> {
return this.authedPost("/api/v1/dimension/stickers/packs/import", {packUrl}).map(r => r.json()).toPromise();
}
} }

View File

@ -0,0 +1 @@
The sticker pack was created by a Matrix user, however the license is unavailable. Sorry.