From 242ad3bf3af2fa53691efdb5c9b290fe323d84a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 16 Sep 2018 02:26:10 -0600 Subject: [PATCH] Admin interface for managing Telegram bridges Currently only one bridge is supported at a time, however in the future we may wish to load balance between bridges or something. --- src/MemoryCache.ts | 3 +- src/api/admin/AdminTelegramService.ts | 112 ++++++++++++++++++ src/bridges/TelegramBridge.ts | 69 +++++++++++ src/bridges/models/telegram.ts | 9 ++ src/db/BridgeStore.ts | 10 ++ src/db/DimensionStore.ts | 2 + .../20180908155745-AddTelegramBridge.ts | 24 ++++ .../20180916014545-AddTelegramBridgeRecord.ts | 23 ++++ src/db/models/TelegramBridgeRecord.ts | 34 ++++++ src/integrations/Bridge.ts | 4 + web/app/admin/bridges/irc/irc.component.html | 2 +- .../manage-selfhosted.component.html | 39 ++++++ .../manage-selfhosted.component.scss | 0 .../manage-selfhosted.component.ts | 59 +++++++++ .../bridges/telegram/telegram.component.html | 48 ++++++++ .../bridges/telegram/telegram.component.scss | 3 + .../bridges/telegram/telegram.component.ts | 76 ++++++++++++ web/app/app.module.ts | 7 ++ web/app/app.routing.ts | 6 + web/app/shared/models/telegram.ts | 8 ++ .../admin/admin-telegram-api.service.ts | 40 +++++++ web/public/img/avatars/telegram.png | Bin 0 -> 8347 bytes 22 files changed, 576 insertions(+), 2 deletions(-) create mode 100644 src/api/admin/AdminTelegramService.ts create mode 100644 src/bridges/TelegramBridge.ts create mode 100644 src/bridges/models/telegram.ts create mode 100644 src/db/migrations/20180908155745-AddTelegramBridge.ts create mode 100644 src/db/migrations/20180916014545-AddTelegramBridgeRecord.ts create mode 100644 src/db/models/TelegramBridgeRecord.ts create mode 100644 web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.html create mode 100644 web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.scss create mode 100644 web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.ts create mode 100644 web/app/admin/bridges/telegram/telegram.component.html create mode 100644 web/app/admin/bridges/telegram/telegram.component.scss create mode 100644 web/app/admin/bridges/telegram/telegram.component.ts create mode 100644 web/app/shared/models/telegram.ts create mode 100644 web/app/shared/services/admin/admin-telegram-api.service.ts create mode 100644 web/public/img/avatars/telegram.png diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts index a4f2d41..7437299 100644 --- a/src/MemoryCache.ts +++ b/src/MemoryCache.ts @@ -48,4 +48,5 @@ export const CACHE_SCALAR_ACCOUNTS = "scalar-accounts"; export const CACHE_WIDGET_TITLES = "widget-titles"; export const CACHE_FEDERATION = "federation"; export const CACHE_IRC_BRIDGE = "irc-bridge"; -export const CACHE_STICKERS = "stickers"; \ No newline at end of file +export const CACHE_STICKERS = "stickers"; +export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge"; \ No newline at end of file diff --git a/src/api/admin/AdminTelegramService.ts b/src/api/admin/AdminTelegramService.ts new file mode 100644 index 0000000..f6cce87 --- /dev/null +++ b/src/api/admin/AdminTelegramService.ts @@ -0,0 +1,112 @@ +import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; +import { AdminService } from "./AdminService"; +import { Cache, CACHE_INTEGRATIONS, CACHE_TELEGRAM_BRIDGE } from "../../MemoryCache"; +import { LogService } from "matrix-js-snippets"; +import { ApiError } from "../ApiError"; +import TelegramBridgeRecord from "../../db/models/TelegramBridgeRecord"; + +interface CreateWithUpstream { + upstreamId: number; +} + +interface CreateSelfhosted { + provisionUrl: string; + sharedSecret: string; + allowPuppets: boolean; +} + +interface BridgeResponse { + id: number; + upstreamId?: number; + provisionUrl?: string; + allowPuppets?: boolean; + sharedSecret?: string; + isEnabled: boolean; +} + +/** + * Administrative API for configuring Telegram bridge instances. + */ +@Path("/api/v1/dimension/admin/telegram") +export class AdminTelegramService { + + @GET + @Path("all") + public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise { + await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const bridges = await TelegramBridgeRecord.findAll(); + return Promise.all(bridges.map(async b => { + return { + id: b.id, + upstreamId: b.upstreamId, + provisionUrl: b.provisionUrl, + allowPuppets: b.allowPuppets, + sharedSecret: b.sharedSecret, + isEnabled: b.isEnabled, + }; + })); + } + + @GET + @Path(":bridgeId") + public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise { + await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const telegramBridge = await TelegramBridgeRecord.findByPrimary(bridgeId); + if (!telegramBridge) throw new ApiError(404, "Telegram Bridge not found"); + + return { + id: telegramBridge.id, + upstreamId: telegramBridge.upstreamId, + provisionUrl: telegramBridge.provisionUrl, + allowPuppets: telegramBridge.allowPuppets, + sharedSecret: telegramBridge.sharedSecret, + isEnabled: telegramBridge.isEnabled, + }; + } + + @POST + @Path(":bridgeId") + public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise { + const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const bridge = await TelegramBridgeRecord.findByPrimary(bridgeId); + if (!bridge) throw new ApiError(404, "Bridge not found"); + + bridge.provisionUrl = request.provisionUrl; + bridge.sharedSecret = request.sharedSecret; + bridge.allowPuppets = request.allowPuppets; + await bridge.save(); + + LogService.info("AdminTelegramService", userId + " updated Telegram Bridge " + bridge.id); + + Cache.for(CACHE_TELEGRAM_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(scalarToken, bridge.id); + } + + @POST + @Path("new/upstream") + public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise { + throw new ApiError(400, "Cannot create a telegram bridge from an upstream"); + } + + @POST + @Path("new/selfhosted") + public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise { + const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const bridge = await TelegramBridgeRecord.create({ + provisionUrl: request.provisionUrl, + sharedSecret: request.sharedSecret, + allowPuppets: request.allowPuppets, + isEnabled: true, + }); + LogService.info("AdminTelegramService", userId + " created a new Telegram Bridge with provisioning URL " + request.provisionUrl); + + Cache.for(CACHE_TELEGRAM_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(scalarToken, bridge.id); + } +} \ No newline at end of file diff --git a/src/bridges/TelegramBridge.ts b/src/bridges/TelegramBridge.ts new file mode 100644 index 0000000..da23608 --- /dev/null +++ b/src/bridges/TelegramBridge.ts @@ -0,0 +1,69 @@ +import { TelegramBridgeConfiguration } from "../integrations/Bridge"; +import TelegramBridgeRecord from "../db/models/TelegramBridgeRecord"; +import { LogService } from "matrix-js-snippets"; +import * as request from "request"; +import { PortalInformationResponse } from "./models/telegram"; + +export class TelegramBridge { + constructor(private requestingUserId: string) { + } + + public async isBridgingEnabled(): Promise { + const bridges = await TelegramBridgeRecord.findAll({where: {isEnabled: true}}); + return !!bridges; + } + + public async getRoomConfiguration(inRoomId: string): Promise { + const bridges = await TelegramBridgeRecord.findAll({where: {isEnabled: true}}); + + const linkedChats: number[] = []; + for (const bridge of bridges) { + try { + const chatInfo = await this.doProvisionRequest(bridge, "GET", `/portal/${inRoomId}`); + linkedChats.push(chatInfo.chat_id); + } catch (e) { + if (!e.errBody || e.errBody["errcode"] !== "portal_not_found") { + throw e.error || e; + } + } + } + + return {linkedChatIds: linkedChats}; + } + + private async doProvisionRequest(bridge: TelegramBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { + const provisionUrl = bridge.provisionUrl; + const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl; + const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); + LogService.info("TelegramBridge", "Doing provision Telegram Bridge request: " + url); + + if (!qs) qs = {}; + if (!qs["user_id"]) qs["user_id"] = this.requestingUserId; + + return new Promise((resolve, reject) => { + request({ + method: method, + url: url, + qs: qs, + json: body, + }, (err, res, _body) => { + if (err) { + LogService.error("TelegramBridge", "Error calling" + url); + LogService.error("TelegramBridge", err); + reject(err); + } else if (!res) { + LogService.error("TelegramBridge", "There is no response for " + url); + reject(new Error("No response provided - is the service online?")); + } else if (res.statusCode !== 200) { + LogService.error("TelegramBridge", "Got status code " + res.statusCode + " when calling " + url); + LogService.error("TelegramBridge", res.body); + if (typeof(res.body) === "string") res.body = JSON.parse(res.body); + reject({errBody: res.body, error: new Error("Request failed")}); + } else { + if (typeof(res.body) === "string") res.body = JSON.parse(res.body); + resolve(res.body); + } + }); + }); + } +} \ No newline at end of file diff --git a/src/bridges/models/telegram.ts b/src/bridges/models/telegram.ts new file mode 100644 index 0000000..a7550f2 --- /dev/null +++ b/src/bridges/models/telegram.ts @@ -0,0 +1,9 @@ +export interface PortalInformationResponse { + mxid: string; + chat_id: number; + peer_type: string; + megagroup: boolean; + username: string; + title: string; + about: string; +} \ No newline at end of file diff --git a/src/db/BridgeStore.ts b/src/db/BridgeStore.ts index dac1c21..67d9859 100644 --- a/src/db/BridgeStore.ts +++ b/src/db/BridgeStore.ts @@ -2,6 +2,7 @@ import { Bridge } from "../integrations/Bridge"; import BridgeRecord from "./models/BridgeRecord"; import { IrcBridge } from "../bridges/IrcBridge"; import { LogService } from "matrix-js-snippets"; +import { TelegramBridge } from "../bridges/TelegramBridge"; export class BridgeStore { @@ -44,6 +45,8 @@ export class BridgeStore { if (integrationType === "irc") { throw new Error("IRC Bridges should be modified with the dedicated API"); + } else if (integrationType === "telegram") { + throw new Error("Telegram bridges should be modified with the dedicated API"); } else throw new Error("Unsupported bridge"); } @@ -51,6 +54,9 @@ export class BridgeStore { if (record.type === "irc") { const irc = new IrcBridge(requestingUserId); return irc.hasNetworks(); + } else if (record.type === "telegram") { + const telegram = new TelegramBridge(requestingUserId); + return telegram.isBridgingEnabled(); } else return true; } @@ -59,6 +65,10 @@ export class BridgeStore { if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs const irc = new IrcBridge(requestingUserId); return irc.getRoomConfiguration(inRoomId); + } else if (record.type === "telegram") { + if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs + const telegram = new TelegramBridge(requestingUserId); + return telegram.getRoomConfiguration(inRoomId); } else return {}; } diff --git a/src/db/DimensionStore.ts b/src/db/DimensionStore.ts index 0ca67fe..8ae51db 100644 --- a/src/db/DimensionStore.ts +++ b/src/db/DimensionStore.ts @@ -21,6 +21,7 @@ import IrcBridgeNetwork from "./models/IrcBridgeNetwork"; import StickerPack from "./models/StickerPack"; import Sticker from "./models/Sticker"; import UserStickerPack from "./models/UserStickerPack"; +import TelegramBridgeRecord from "./models/TelegramBridgeRecord"; class _DimensionStore { private sequelize: Sequelize; @@ -53,6 +54,7 @@ class _DimensionStore { StickerPack, Sticker, UserStickerPack, + TelegramBridgeRecord, ]); } diff --git a/src/db/migrations/20180908155745-AddTelegramBridge.ts b/src/db/migrations/20180908155745-AddTelegramBridge.ts new file mode 100644 index 0000000..7ced66f --- /dev/null +++ b/src/db/migrations/20180908155745-AddTelegramBridge.ts @@ -0,0 +1,24 @@ +import { QueryInterface } from "sequelize"; +import { DataType } from "sequelize-typescript"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.createTable("dimension_telegram_bridges", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "upstreamId": { + type: DataType.INTEGER, allowNull: true, + references: {model: "dimension_upstreams", key: "id"}, + onUpdate: "cascade", onDelete: "cascade", + }, + "provisionUrl": {type: DataType.STRING, allowNull: true}, + "sharedSecret": {type: DataType.STRING, allowNull: true}, + "allowPuppets": {type: DataType.BOOLEAN, allowNull: true}, + "isEnabled": {type: DataType.BOOLEAN, allowNull: false}, + })); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.dropTable("dimension_telegram_bridges")); + } +} \ No newline at end of file diff --git a/src/db/migrations/20180916014545-AddTelegramBridgeRecord.ts b/src/db/migrations/20180916014545-AddTelegramBridgeRecord.ts new file mode 100644 index 0000000..16d35ba --- /dev/null +++ b/src/db/migrations/20180916014545-AddTelegramBridgeRecord.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_bridges", [ + { + type: "telegram", + name: "Telegram Bridge", + avatarUrl: "/img/avatars/telegram.png", + isEnabled: true, + isPublic: true, + description: "Bridges Telegram chats and channels to rooms on Matrix", + }, + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_bridges", { + type: "telegram", + })); + } +} \ No newline at end of file diff --git a/src/db/models/TelegramBridgeRecord.ts b/src/db/models/TelegramBridgeRecord.ts new file mode 100644 index 0000000..f70d316 --- /dev/null +++ b/src/db/models/TelegramBridgeRecord.ts @@ -0,0 +1,34 @@ +import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; +import Upstream from "./Upstream"; + +@Table({ + tableName: "dimension_telegram_bridges", + underscoredAll: false, + timestamps: false, +}) +export default class TelegramBridgeRecord extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull + @Column + @ForeignKey(() => Upstream) + upstreamId?: number; + + @AllowNull + @Column + provisionUrl?: string; + + @AllowNull + @Column + sharedSecret?: string; + + @AllowNull + @Column + allowPuppets?: boolean; + + @Column + isEnabled: boolean; +} \ No newline at end of file diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts index fef2643..70aae2e 100644 --- a/src/integrations/Bridge.ts +++ b/src/integrations/Bridge.ts @@ -20,4 +20,8 @@ export class Bridge extends Integration { export interface IrcBridgeConfiguration { availableNetworks: AvailableNetworks; links: LinkedChannels; +} + +export interface TelegramBridgeConfiguration { + linkedChatIds: number[]; } \ No newline at end of file diff --git a/web/app/admin/bridges/irc/irc.component.html b/web/app/admin/bridges/irc/irc.component.html index 5a324f7..798f093 100644 --- a/web/app/admin/bridges/irc/irc.component.html +++ b/web/app/admin/bridges/irc/irc.component.html @@ -20,7 +20,7 @@ - No bridge configurations. + No bridge configurations. diff --git a/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.html b/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.html new file mode 100644 index 0000000..aa7a830 --- /dev/null +++ b/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.html @@ -0,0 +1,39 @@ +
+
+

{{ isAdding ? "Add a new" : "Edit" }} self-hosted Telegram bridge

+
+
+

Self-hosted Telegram bridges must have provisioning enabled in the configuration.

+ + + + + + +
+ +
\ No newline at end of file diff --git a/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.scss b/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.ts b/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.ts new file mode 100644 index 0000000..06af56b --- /dev/null +++ b/web/app/admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component.ts @@ -0,0 +1,59 @@ +import { Component } from "@angular/core"; +import { ToasterService } from "angular2-toaster"; +import { DialogRef, ModalComponent } from "ngx-modialog"; +import { BSModalContext } from "ngx-modialog/plugins/bootstrap"; +import { AdminTelegramApiService } from "../../../../shared/services/admin/admin-telegram-api.service"; + +export class ManageSelfhostedTelegramBridgeDialogContext extends BSModalContext { + public provisionUrl: string; + public sharedSecret: string; + public allowPuppets = false; + public bridgeId: number; +} + +@Component({ + templateUrl: "./manage-selfhosted.component.html", + styleUrls: ["./manage-selfhosted.component.scss"], +}) +export class AdminTelegramBridgeManageSelfhostedComponent implements ModalComponent { + + public isSaving = false; + public provisionUrl: string; + public sharedSecret: string; + public allowPuppets = false; + public bridgeId: number; + public isAdding = false; + + constructor(public dialog: DialogRef, + private telegramApi: AdminTelegramApiService, + private toaster: ToasterService) { + this.provisionUrl = dialog.context.provisionUrl; + this.sharedSecret = dialog.context.sharedSecret; + this.allowPuppets = dialog.context.allowPuppets; + this.bridgeId = dialog.context.bridgeId; + this.isAdding = !this.bridgeId; + } + + public add() { + this.isSaving = true; + if (this.isAdding) { + this.telegramApi.newSelfhosted(this.provisionUrl, this.sharedSecret, this.allowPuppets).then(() => { + this.toaster.pop("success", "Telegram bridge added"); + this.dialog.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.toaster.pop("error", "Failed to create Telegram bridge"); + }); + } else { + this.telegramApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret, this.allowPuppets).then(() => { + this.toaster.pop("success", "Telegram bridge updated"); + this.dialog.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.toaster.pop("error", "Failed to update Telegram bridge"); + }); + } + } +} diff --git a/web/app/admin/bridges/telegram/telegram.component.html b/web/app/admin/bridges/telegram/telegram.component.html new file mode 100644 index 0000000..3b56f70 --- /dev/null +++ b/web/app/admin/bridges/telegram/telegram.component.html @@ -0,0 +1,48 @@ +
+ +
+
+ +
+

+ mautrix-telegram + is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be + done through a "relay bot" (all Matrix users are represented through a single bot in Telegram) + or by making use of a user's real Telegram account (known as "puppeting"). Currently + only one Telegram bridge can be configured at a time. +

+ + + + + + + + + + + + + + + + + + + +
NameEnabled FeaturesActions
No bridge configurations.
+ {{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }} + ({{ bridge.provisionUrl }}) + + {{ getEnabledFeaturesString(bridge) }} + + + + +
+ +
+
+
\ No newline at end of file diff --git a/web/app/admin/bridges/telegram/telegram.component.scss b/web/app/admin/bridges/telegram/telegram.component.scss new file mode 100644 index 0000000..788d7ed --- /dev/null +++ b/web/app/admin/bridges/telegram/telegram.component.scss @@ -0,0 +1,3 @@ +.editButton { + cursor: pointer; +} \ No newline at end of file diff --git a/web/app/admin/bridges/telegram/telegram.component.ts b/web/app/admin/bridges/telegram/telegram.component.ts new file mode 100644 index 0000000..842bda8 --- /dev/null +++ b/web/app/admin/bridges/telegram/telegram.component.ts @@ -0,0 +1,76 @@ +import { Component, OnInit } from "@angular/core"; +import { ToasterService } from "angular2-toaster"; +import { Modal, overlayConfigFactory } from "ngx-modialog"; +import { + AdminTelegramBridgeManageSelfhostedComponent, + ManageSelfhostedTelegramBridgeDialogContext +} from "./manage-selfhosted/manage-selfhosted.component"; +import { FE_TelegramBridge } from "../../../shared/models/telegram"; +import { AdminTelegramApiService } from "../../../shared/services/admin/admin-telegram-api.service"; + +@Component({ + templateUrl: "./telegram.component.html", + styleUrls: ["./telegram.component.scss"], +}) +export class AdminTelegramBridgeComponent implements OnInit { + + public isLoading = true; + public isUpdating = false; + public configurations: FE_TelegramBridge[] = []; + + constructor(private telegramApi: AdminTelegramApiService, + private toaster: ToasterService, + private modal: Modal) { + } + + public ngOnInit() { + this.reload().then(() => this.isLoading = false); + } + + private async reload(): Promise { + try { + this.configurations = await this.telegramApi.getBridges(); + } catch (err) { + console.error(err); + this.toaster.pop("error", "Error loading bridges"); + } + } + + public addSelfHostedBridge() { + this.modal.open(AdminTelegramBridgeManageSelfhostedComponent, overlayConfigFactory({ + isBlocking: true, + size: 'lg', + + provisionUrl: '', + sharedSecret: '', + allowPuppets: false, + }, ManageSelfhostedTelegramBridgeDialogContext)).result.then(() => { + this.reload().catch(err => { + console.error(err); + this.toaster.pop("error", "Failed to get an update Telegram bridge list"); + }); + }); + } + + public getEnabledFeaturesString(bridge: FE_TelegramBridge): string { + if (bridge.allowPuppets) return "Puppeting"; + return ""; + } + + public editBridge(bridge: FE_TelegramBridge) { + this.modal.open(AdminTelegramBridgeManageSelfhostedComponent, overlayConfigFactory({ + isBlocking: true, + size: 'lg', + + provisionUrl: bridge.provisionUrl, + sharedSecret: bridge.sharedSecret, + allowPuppets: bridge.allowPuppets, + bridgeId: bridge.id, + }, ManageSelfhostedTelegramBridgeDialogContext)).result.then(() => { + this.reload().catch(err => { + console.error(err); + this.toaster.pop("error", "Failed to get an update Telegram bridge list"); + }); + }); + } +} diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 6f6451f..0eb2aba 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -78,6 +78,9 @@ import { MediaService } from "./shared/services/media.service"; import { StickerApiService } from "./shared/services/integrations/sticker-api.service"; import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.component"; import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-picker/sticker-picker.component"; +import { AdminTelegramApiService } from "./shared/services/admin/admin-telegram-api.service"; +import { AdminTelegramBridgeComponent } from "./admin/bridges/telegram/telegram.component"; +import { AdminTelegramBridgeManageSelfhostedComponent } from "./admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component"; @NgModule({ imports: [ @@ -145,6 +148,8 @@ import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-p AdminStickerPackPreviewComponent, StickerpickerComponent, StickerPickerWidgetWrapperComponent, + AdminTelegramBridgeComponent, + AdminTelegramBridgeManageSelfhostedComponent, // Vendor ], @@ -164,6 +169,7 @@ import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-p AdminStickersApiService, MediaService, StickerApiService, + AdminTelegramApiService, {provide: Window, useValue: window}, // Vendor @@ -181,6 +187,7 @@ import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-p AdminIrcBridgeNetworksComponent, AdminIrcBridgeAddSelfhostedComponent, AdminStickerPackPreviewComponent, + AdminTelegramBridgeManageSelfhostedComponent, ] }) export class AppModule { diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index b46e185..95c5c7f 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -27,6 +27,7 @@ import { IrcBridgeConfigComponent } from "./configs/bridge/irc/irc.bridge.compon import { AdminStickerPacksComponent } from "./admin/sticker-packs/sticker-packs.component"; import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.component"; import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-picker/sticker-picker.component"; +import { AdminTelegramBridgeComponent } from "./admin/bridges/telegram/telegram.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -87,6 +88,11 @@ const routes: Routes = [ component: AdminIrcBridgeComponent, data: {breadcrumb: "IRC Bridge", name: "IRC Bridge"}, }, + { + path: "telegram", + component: AdminTelegramBridgeComponent, + data: {breadcrumb: "Telegram Bridge", name: "Telegram Bridge"}, + }, ], }, { diff --git a/web/app/shared/models/telegram.ts b/web/app/shared/models/telegram.ts new file mode 100644 index 0000000..6be69dd --- /dev/null +++ b/web/app/shared/models/telegram.ts @@ -0,0 +1,8 @@ +export interface FE_TelegramBridge { + id: number; + upstreamId?: number; + provisionUrl?: string; + allowPuppets?: boolean; + sharedSecret?: string; + isEnabled: boolean; +} \ No newline at end of file diff --git a/web/app/shared/services/admin/admin-telegram-api.service.ts b/web/app/shared/services/admin/admin-telegram-api.service.ts new file mode 100644 index 0000000..cc23d3e --- /dev/null +++ b/web/app/shared/services/admin/admin-telegram-api.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from "@angular/core"; +import { Http } from "@angular/http"; +import { AuthedApi } from "../authed-api"; +import { FE_Upstream } from "../../models/admin-responses"; +import { FE_TelegramBridge } from "../../models/telegram"; + +@Injectable() +export class AdminTelegramApiService extends AuthedApi { + constructor(http: Http) { + super(http); + } + + public getBridges(): Promise { + return this.authedGet("/api/v1/dimension/admin/telegram/all").map(r => r.json()).toPromise(); + } + + public getBridge(bridgeId: number): Promise { + return this.authedGet("/api/v1/dimension/admin/telegram/" + bridgeId).map(r => r.json()).toPromise(); + } + + public newFromUpstream(upstream: FE_Upstream): Promise { + return this.authedPost("/api/v1/dimension/admin/telegram/new/upstream", {upstreamId: upstream.id}).map(r => r.json()).toPromise(); + } + + public newSelfhosted(provisionUrl: string, sharedSecret: string, allowPuppets: boolean): Promise { + return this.authedPost("/api/v1/dimension/admin/telegram/new/selfhosted", { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + allowPuppets: allowPuppets, + }).map(r => r.json()).toPromise(); + } + + public updateSelfhosted(bridgeId: number, provisionUrl: string, sharedSecret: string, allowPuppets: boolean): Promise { + return this.authedPost("/api/v1/dimension/admin/telegram/" + bridgeId, { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + allowPuppets: allowPuppets, + }).map(r => r.json()).toPromise(); + } +} diff --git a/web/public/img/avatars/telegram.png b/web/public/img/avatars/telegram.png new file mode 100644 index 0000000000000000000000000000000000000000..6553bdf7f606d686bf7196da1ace2414f8f00fcc GIT binary patch literal 8347 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%0D| literal 0 HcmV?d00001