Merge branch 'travis/meaningless-branch-name'

This commit is contained in:
Travis Ralston 2021-12-13 13:30:43 -07:00
commit 7be069b319
164 changed files with 5075 additions and 1750 deletions

View File

@ -1,7 +0,0 @@
{
"syncToken": "s29_4025_0_1_40_1_26_19_1",
"filter": null,
"appserviceUsers": {},
"appserviceTransactions": {},
"kvStore": {}
}

View File

@ -1,6 +0,0 @@
{
"syncToken": "s3510138_39116385_25484_5602626_218935_269_119102_8310593_32",
"filter": null,
"appserviceUsers": {},
"appserviceTransactions": {}
}

Binary file not shown.

Binary file not shown.

122
docs/local.md Normal file
View File

@ -0,0 +1,122 @@
# Developer/local environment setup
**Disclaimer**: This guide assumes a fairly high level of pre-existing knowledge regarding development practices, Matrix,
integration managers, and troubleshooting. The guide covers the happy path, though sometimes it doesn't work out that way.
Troubleshooting the system is best before jumping into support rooms: it helps build context on how all this works.
Prerequisites:
* A local Synapse.
* A local Element Web.
* All the other dependencies which would be required by a production install.
**Note**: As a local environment, you do not need to use https for any of the setup. In fact, it is recommended to stick
to plain http as it involves less certificate issues. For added benefit, set your homeserver name to `localhost`.
## Part 1: Configuration
Copy the default configuration and give it an edit. The major settings will be the `web`, `homeserver`, and `admins`
sections.
It is recommended to run Dimension on `0.0.0.0:8184` for demonstration purposes. Your homeserver's `clientServerUrl`
*and* `federationUrl` should be pointed to your local Synapse. This will bypass a lot of the federation-level stuff
by pointing your Dimension directly at the Synapse instance rather than it trying to resolve `localhost` on the public
federation.
The `accessToken` should be a dedicated user **that has not ever opened Dimension**. Set this up now before configuring
the client.
```bash
# Edit the configuration to your specifications.
# Be sure to add yourself as an admin!
cp config/default.yaml config/development.yaml
nano config/development.yaml
```
## Part 2: Installation and running
In a bash terminal:
```bash
npm install
npm run start:apponly
```
If that starts up without issue (ie: doesn't crash) then open a second terminal with:
```bash
npm run start:web
```
You should now have **2** terminals, one for the web app and one for the backend. Both should live reload if you make
changes to the respective layers.
The web server will start on port `8082`.
## Part 3: Element configuration
In your local `config.json`, add/edit the following keys:
```json
{
"integrations_ui_url": "http://localhost:8082/element",
"integrations_rest_url": "http://localhost:8082/api/v1/scalar",
"integrations_widgets_urls": ["http://localhost:8082/widgets"],
"integrations_jitsi_widget_url": "http://localhost:8082/widgets/jitsi"
}
```
## Part 4: Using Dimension
If everything went according to plan, Dimension should now be accessible from the "Add widgets, bridges & bots" button on
the room information card.
You should see a cog/gear icon in the top right of the dialog window. This indicates that you are an admin. If you do not
see this, fix the Dimension config and restart the `npm run start:apponly` process.
## Part 5: Configuring integrations in Dimension
Click the gear icon in the Dimension window within Element. The menu on the left side should guide you through the process
of setting up and configuring integrations.
**Danger**: Upstream (matrix.org) integrations will not work. Do not enable them in local environments.
**Note**: Dimension enforces a maximum of one bridge instance per type. It does this by disabling buttons despite indicating
that there's potential for multiple to be added. Adding a self-hosted bridge will disable all future options of adding a
second bridge, but it should be possible to edit the added instance.
For the purposes of this guide, we'll set up Hookshot and a custom bot.
## Part 6: Hookshot
First, set up a local instance of [matrix-hookshot](https://github.com/Half-Shot/matrix-hookshot) within your test environment.
It is recommended to proxy some of the endpoints through ngrok (or similar) to make external integration a lot easier.
For testing purposes it's often best to use a single Hookshot instance, though Dimension will assume that you'll have
multiple instances, one for each capable service, however it is perfectly fine to point all the Dimension configs at the
same Hookshot instance.
The Hookshot-capable bridges can be configured from here:
![](https://i.imgur.com/42dTDuk.png)
*UI may vary. Note the subtle difference in descriptions for the two webhook bridges.*
Simply configure the bridges from that view and click "Browse integrations" in the top left to go work with the bridges.
They should appear as integrations within Dimension.
## Part 7: Custom bots
Custom bots can be configured from the admin section under "Custom bots". Dimension will need an access token, and will
assume that the bots do not need special power levels or invite handling - they will be required to operate at the default
power level for rooms, and auto-accept invites on their own. Dimension will take care of removing the bot from the room
when the time comes, and the bot should be okay with this happening outside of its process.
Maubot bots are great options for inclusion here.
## Part 8: Troubleshooting
Occasionally the live reload functionality either doesn't work or causes problems. Re-open Dimension within Element to
fix most issues, and restart the relevant processes if needed.
Note that Dimension will do its best to avoid crashing, but will produce cryptic errors if an integration is down or
misconfigured.
For all other troubleshooting, please visit the support room.

View File

@ -16,8 +16,8 @@
"lint:app": "eslint src",
"lint:web": "eslint web",
"i18n": "npm run-script i18n:init && npm run-script i18n:extract",
"i18n:init": "ngx-translate-extract --input ./web --output ./web/public/assets/i18n/template.json --key-as-default-value --replace --format json",
"i18n:extract": "ngx-translate-extract --input ./web --output ./web/public/assets/i18n/en.json --clean --format json"
"i18n:init": "ngx-translate-extract --input ./web --output ./web/assets/i18n/template.json --key-as-default-value --replace --format json",
"i18n:extract": "ngx-translate-extract --input ./web --output ./web/assets/i18n/en.json --key-as-default-value --clean --format json"
},
"repository": {
"type": "git",

View File

@ -51,6 +51,9 @@ export const CACHE_FEDERATION = "federation";
export const CACHE_IRC_BRIDGE = "irc-bridge";
export const CACHE_STICKERS = "stickers";
export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge";
export const CACHE_HOOKSHOT_GITHUB_BRIDGE = "hookshot-github-bridge";
export const CACHE_HOOKSHOT_WEBHOOK_BRIDGE = "hookshot-webhook-bridge";
export const CACHE_HOOKSHOT_JIRA_BRIDGE = "hookshot-jira-bridge";
export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge";
export const CACHE_SIMPLE_BOTS = "simple-bots";
export const CACHE_SLACK_BRIDGE = "slack-bridge";

View File

@ -0,0 +1,110 @@
import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
import { Cache, CACHE_HOOKSHOT_GITHUB_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { LogService } from "matrix-bot-sdk";
import { ApiError } from "../ApiError";
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
import HookshotGithubBridgeRecord from "../../db/models/HookshotGithubBridgeRecord";
interface CreateWithUpstream {
upstreamId: number;
}
interface CreateSelfhosted {
provisionUrl: string;
sharedSecret: string;
}
interface BridgeResponse {
id: number;
upstreamId?: number;
provisionUrl?: string;
sharedSecret?: string;
isEnabled: boolean;
}
/**
* Administrative API for configuring Hookshot Github bridge instances.
*/
@Path("/api/v1/dimension/admin/hookshot/github")
export class AdminHookshotGithubService {
@Context
private context: ServiceContext;
@GET
@Path("all")
@Security([ROLE_USER, ROLE_ADMIN])
public async getBridges(): Promise<BridgeResponse[]> {
const bridges = await HookshotGithubBridgeRecord.findAll();
return Promise.all(bridges.map(async b => {
return {
id: b.id,
upstreamId: b.upstreamId,
provisionUrl: b.provisionUrl,
sharedSecret: b.sharedSecret,
isEnabled: b.isEnabled,
};
}));
}
@GET
@Path(":bridgeId")
@Security([ROLE_USER, ROLE_ADMIN])
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const githubBridge = await HookshotGithubBridgeRecord.findByPk(bridgeId);
if (!githubBridge) throw new ApiError(404, "Github Bridge not found");
return {
id: githubBridge.id,
upstreamId: githubBridge.upstreamId,
provisionUrl: githubBridge.provisionUrl,
sharedSecret: githubBridge.sharedSecret,
isEnabled: githubBridge.isEnabled,
};
}
@POST
@Path(":bridgeId")
@Security([ROLE_USER, ROLE_ADMIN])
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await HookshotGithubBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found");
bridge.provisionUrl = request.provisionUrl;
bridge.sharedSecret = request.sharedSecret;
await bridge.save();
LogService.info("AdminHookshotGithubService", userId + " updated Hookshot Github Bridge " + bridge.id);
Cache.for(CACHE_HOOKSHOT_GITHUB_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(bridge.id);
}
@POST
@Path("new/upstream")
@Security([ROLE_USER, ROLE_ADMIN])
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
throw new ApiError(400, "Cannot create a github bridge from an upstream");
}
@POST
@Path("new/selfhosted")
@Security([ROLE_USER, ROLE_ADMIN])
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await HookshotGithubBridgeRecord.create({
provisionUrl: request.provisionUrl,
sharedSecret: request.sharedSecret,
isEnabled: true,
});
LogService.info("AdminHookshotGithubService", userId + " created a new Hookshot Github Bridge with provisioning URL " + request.provisionUrl);
Cache.for(CACHE_HOOKSHOT_GITHUB_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(bridge.id);
}
}

View File

@ -0,0 +1,110 @@
import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
import { Cache, CACHE_HOOKSHOT_JIRA_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { LogService } from "matrix-bot-sdk";
import { ApiError } from "../ApiError";
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
import HookshotJiraBridgeRecord from "../../db/models/HookshotJiraBridgeRecord";
interface CreateWithUpstream {
upstreamId: number;
}
interface CreateSelfhosted {
provisionUrl: string;
sharedSecret: string;
}
interface BridgeResponse {
id: number;
upstreamId?: number;
provisionUrl?: string;
sharedSecret?: string;
isEnabled: boolean;
}
/**
* Administrative API for configuring Hookshot Jira bridge instances.
*/
@Path("/api/v1/dimension/admin/hookshot/jira")
export class AdminHookshotJiraService {
@Context
private context: ServiceContext;
@GET
@Path("all")
@Security([ROLE_USER, ROLE_ADMIN])
public async getBridges(): Promise<BridgeResponse[]> {
const bridges = await HookshotJiraBridgeRecord.findAll();
return Promise.all(bridges.map(async b => {
return {
id: b.id,
upstreamId: b.upstreamId,
provisionUrl: b.provisionUrl,
sharedSecret: b.sharedSecret,
isEnabled: b.isEnabled,
};
}));
}
@GET
@Path(":bridgeId")
@Security([ROLE_USER, ROLE_ADMIN])
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const jiraBridge = await HookshotJiraBridgeRecord.findByPk(bridgeId);
if (!jiraBridge) throw new ApiError(404, "Jira Bridge not found");
return {
id: jiraBridge.id,
upstreamId: jiraBridge.upstreamId,
provisionUrl: jiraBridge.provisionUrl,
sharedSecret: jiraBridge.sharedSecret,
isEnabled: jiraBridge.isEnabled,
};
}
@POST
@Path(":bridgeId")
@Security([ROLE_USER, ROLE_ADMIN])
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await HookshotJiraBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found");
bridge.provisionUrl = request.provisionUrl;
bridge.sharedSecret = request.sharedSecret;
await bridge.save();
LogService.info("AdminHookshotJiraService", userId + " updated Hookshot Jira Bridge " + bridge.id);
Cache.for(CACHE_HOOKSHOT_JIRA_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(bridge.id);
}
@POST
@Path("new/upstream")
@Security([ROLE_USER, ROLE_ADMIN])
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
throw new ApiError(400, "Cannot create a jira bridge from an upstream");
}
@POST
@Path("new/selfhosted")
@Security([ROLE_USER, ROLE_ADMIN])
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await HookshotJiraBridgeRecord.create({
provisionUrl: request.provisionUrl,
sharedSecret: request.sharedSecret,
isEnabled: true,
});
LogService.info("AdminHookshotJiraService", userId + " created a new Hookshot Jira Bridge with provisioning URL " + request.provisionUrl);
Cache.for(CACHE_HOOKSHOT_JIRA_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(bridge.id);
}
}

View File

@ -0,0 +1,110 @@
import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
import { Cache, CACHE_HOOKSHOT_WEBHOOK_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { LogService } from "matrix-bot-sdk";
import { ApiError } from "../ApiError";
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
import HookshotWebhookBridgeRecord from "../../db/models/HookshotWebhookBridgeRecord";
interface CreateWithUpstream {
upstreamId: number;
}
interface CreateSelfhosted {
provisionUrl: string;
sharedSecret: string;
}
interface BridgeResponse {
id: number;
upstreamId?: number;
provisionUrl?: string;
sharedSecret?: string;
isEnabled: boolean;
}
/**
* Administrative API for configuring Hookshot Webhook bridge instances.
*/
@Path("/api/v1/dimension/admin/hookshot/webhook")
export class AdminHookshotWebhookService {
@Context
private context: ServiceContext;
@GET
@Path("all")
@Security([ROLE_USER, ROLE_ADMIN])
public async getBridges(): Promise<BridgeResponse[]> {
const bridges = await HookshotWebhookBridgeRecord.findAll();
return Promise.all(bridges.map(async b => {
return {
id: b.id,
upstreamId: b.upstreamId,
provisionUrl: b.provisionUrl,
sharedSecret: b.sharedSecret,
isEnabled: b.isEnabled,
};
}));
}
@GET
@Path(":bridgeId")
@Security([ROLE_USER, ROLE_ADMIN])
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const bridge = await HookshotWebhookBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Webhook Bridge not found");
return {
id: bridge.id,
upstreamId: bridge.upstreamId,
provisionUrl: bridge.provisionUrl,
sharedSecret: bridge.sharedSecret,
isEnabled: bridge.isEnabled,
};
}
@POST
@Path(":bridgeId")
@Security([ROLE_USER, ROLE_ADMIN])
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await HookshotWebhookBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found");
bridge.provisionUrl = request.provisionUrl;
bridge.sharedSecret = request.sharedSecret;
await bridge.save();
LogService.info("AdminHookshotWebhookService", userId + " updated Hookshot Webhook Bridge " + bridge.id);
Cache.for(CACHE_HOOKSHOT_WEBHOOK_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(bridge.id);
}
@POST
@Path("new/upstream")
@Security([ROLE_USER, ROLE_ADMIN])
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
throw new ApiError(400, "Cannot create a webhook bridge from an upstream");
}
@POST
@Path("new/selfhosted")
@Security([ROLE_USER, ROLE_ADMIN])
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await HookshotWebhookBridgeRecord.create({
provisionUrl: request.provisionUrl,
sharedSecret: request.sharedSecret,
isEnabled: true,
});
LogService.info("AdminHookshotWebhookService", userId + " created a new Hookshot Webhook Bridge with provisioning URL " + request.provisionUrl);
Cache.for(CACHE_HOOKSHOT_WEBHOOK_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(bridge.id);
}
}

View File

@ -0,0 +1,90 @@
import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { ApiError } from "../ApiError";
import { LogService } from "matrix-bot-sdk";
import { ROLE_USER } from "../security/MatrixSecurity";
import {
HookshotGithubAuthUrls,
HookshotGithubOrgReposDto,
HookshotGithubRoomConfig
} from "../../bridges/models/hookshot";
import { HookshotGithubBridge } from "../../bridges/HookshotGithubBridge";
interface BridgeRoomRequest {
orgId: string;
repoId: string;
}
/**
* API for interacting with the Hookshot/Github bridge
*/
@Path("/api/v1/dimension/hookshot/github")
export class DimensionHookshotGithubService {
@Context
private context: ServiceContext;
@GET
@Path("auth")
@Security(ROLE_USER)
public async getAuthUrls(): Promise<HookshotGithubAuthUrls> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotGithubBridge(userId);
return await hookshot.getAuthUrls();
} catch (e) {
LogService.error("DimensionHookshotGithubService", e);
throw new ApiError(400, "Error getting auth info");
}
}
@GET
@Path("locations")
@Security(ROLE_USER)
public async getUserRepos(): Promise<{ locations: HookshotGithubOrgReposDto[] }> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotGithubBridge(userId);
const locations = await hookshot.getInstalledLocations();
return {locations};
} catch (e) {
LogService.error("DimensionHookshotGithubService", e);
throw new ApiError(400, "Error getting repo information", "T2B_MISSING_AUTH");
}
}
@POST
@Path("room/:roomId/connect")
@Security(ROLE_USER)
public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<HookshotGithubRoomConfig> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotGithubBridge(userId);
return hookshot.bridgeRoom(roomId, request.orgId, request.repoId);
} catch (e) {
LogService.error("DimensionHookshotGithubService", e);
throw new ApiError(400, "Error bridging room");
}
}
@DELETE
@Path("room/:roomId/connections/all")
@Security(ROLE_USER)
public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<any> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotGithubBridge(userId);
const connections = await hookshot.getRoomConfigurations(roomId);
for (const conn of connections) {
await hookshot.unbridgeRoom(roomId, conn.id);
}
return {}; // 200 OK
} catch (e) {
LogService.error("DimensionHookshotGithubService", e);
throw new ApiError(400, "Error unbridging room");
}
}
}

View File

@ -0,0 +1,96 @@
import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { ApiError } from "../ApiError";
import { LogService } from "matrix-bot-sdk";
import { ROLE_USER } from "../security/MatrixSecurity";
import { HookshotJiraBridge } from "../../bridges/HookshotJiraBridge";
import { HookshotJiraInstance, HookshotJiraProject, HookshotJiraRoomConfig } from "../../bridges/models/hookshot";
interface BridgeRoomRequest {
instanceName: string;
projectKey: string;
}
/**
* API for interacting with the Hookshot/Jira bridge
*/
@Path("/api/v1/dimension/hookshot/jira")
export class DimensionHookshotJiraService {
@Context
private context: ServiceContext;
@GET
@Path("auth")
@Security(ROLE_USER)
public async getAuthUrl(): Promise<{ authUrl: string }> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotJiraBridge(userId);
const authUrl = await hookshot.getAuthUrl();
return {authUrl};
} catch (e) {
LogService.error("DimensionHookshotJiraService", e);
throw new ApiError(400, "Error getting auth info");
}
}
@GET
@Path("instances")
@Security(ROLE_USER)
public async getInstances(): Promise<{ instances: HookshotJiraInstance[] }> {
const userId = this.context.request.user.userId;
const hookshot = new HookshotJiraBridge(userId);
const userInfo = await hookshot.getLoggedInUserInfo();
if (!userInfo.loggedIn) {
throw new ApiError(403, "Not logged in", "T2B_NOT_LOGGED_IN");
}
return {instances: userInfo.instances};
}
@GET
@Path("instance/:instanceName/projects")
@Security(ROLE_USER)
public async getProjects(@PathParam("instanceName") instanceName: string): Promise<{ projects: HookshotJiraProject[] }> {
const userId = this.context.request.user.userId;
const hookshot = new HookshotJiraBridge(userId);
const projects = await hookshot.getProjects(instanceName);
return {projects};
}
@POST
@Path("room/:roomId/connect")
@Security(ROLE_USER)
public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<HookshotJiraRoomConfig> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotJiraBridge(userId);
return hookshot.bridgeRoom(roomId, request.instanceName, request.projectKey);
} catch (e) {
LogService.error("DimensionHookshotJiraService", e);
throw new ApiError(400, "Error bridging room");
}
}
@DELETE
@Path("room/:roomId/connections/all")
@Security(ROLE_USER)
public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<any> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotJiraBridge(userId);
const connections = await hookshot.getRoomConfigurations(roomId);
for (const conn of connections) {
await hookshot.unbridgeRoom(roomId, conn.id);
}
return {}; // 200 OK
} catch (e) {
LogService.error("DimensionHookshotJiraService", e);
throw new ApiError(400, "Error unbridging room");
}
}
}

View File

@ -0,0 +1,51 @@
import { Context, DELETE, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { ApiError } from "../ApiError";
import { LogService } from "matrix-bot-sdk";
import { ROLE_USER } from "../security/MatrixSecurity";
import { HookshotWebhookRoomConfig } from "../../bridges/models/hookshot";
import { HookshotWebhookBridge } from "../../bridges/HookshotWebhookBridge";
interface BridgeRoomRequest {
name?: string;
}
/**
* API for interacting with the Hookshot/Webhook bridge
*/
@Path("/api/v1/dimension/hookshot/webhook")
export class DimensionHookshotWebhookService {
@Context
private context: ServiceContext;
@POST
@Path("room/:roomId/connect")
@Security(ROLE_USER)
public async createWebhook(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<HookshotWebhookRoomConfig> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotWebhookBridge(userId);
return hookshot.newConnection(roomId, request.name);
} catch (e) {
LogService.error("DimensionHookshotWebhookService", e);
throw new ApiError(400, "Error bridging room");
}
}
@DELETE
@Path("room/:roomId/connection/:connectionId/disconnect")
@Security(ROLE_USER)
public async removeWebhook(@PathParam("roomId") roomId: string, @PathParam("connectionId") connectionId: string): Promise<any> {
const userId = this.context.request.user.userId;
try {
const hookshot = new HookshotWebhookBridge(userId);
await hookshot.removeConnection(roomId, connectionId);
return {}; // 200 OK
} catch (e) {
LogService.error("DimensionHookshotWebhookService", e);
throw new ApiError(400, "Error unbridging room");
}
}
}

View File

@ -52,6 +52,7 @@ export class DimensionSlackService {
@DELETE
@Path("room/:roomId/link")
@Security(ROLE_USER)
public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<any> {
const userId = this.context.request.user.userId;

View File

@ -0,0 +1,77 @@
import { LogService } from "matrix-bot-sdk";
import * as request from "request";
import { HookshotConnectionsResponse, HookshotConnectionTypeDefinition } from "./models/hookshot";
import { IHookshotBridgeRecord } from "../db/models/IHookshotBridgeRecord";
export abstract class HookshotBridge {
protected constructor(private requestingUserId: string) {
}
protected abstract getDefaultBridge(): Promise<IHookshotBridgeRecord>;
protected async getAllRoomConfigurations(inRoomId: string): Promise<HookshotConnectionsResponse> {
const bridge = await this.getDefaultBridge();
try {
return await this.doProvisionRequest<HookshotConnectionsResponse>(bridge, "GET", `/v1/${inRoomId}/connections`);
} catch (e) {
if (e.errBody['errcode'] === "HS_NOT_IN_ROOM") {
return [];
}
throw e;
}
}
protected async getAllServiceInformation(): Promise<HookshotConnectionTypeDefinition[]> {
const bridge = await this.getDefaultBridge();
const connections = await this.doProvisionRequest(bridge, "GET", `/v1/connectiontypes`);
return Object.values(connections);
}
protected async doProvisionRequest<T>(bridge: IHookshotBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise<T> {
const provisionUrl = bridge.provisionUrl;
const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl;
const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint);
LogService.info("HookshotBridge", "Doing provision Hookshot Bridge request: " + url);
if (!qs) qs = {};
if (qs["userId"] === false) delete qs["userId"];
else if (!qs["userId"]) qs["userId"] = this.requestingUserId;
return new Promise<T>((resolve, reject) => {
request({
method: method,
url: url,
qs: qs,
json: body,
headers: {
"Authorization": `Bearer ${bridge.sharedSecret}`,
},
}, (err, res, _body) => {
try {
if (err) {
LogService.error("HookshotBridge", "Error calling" + url);
LogService.error("HookshotBridge", err);
reject(err);
} else if (!res) {
LogService.error("HookshotBridge", "There is no response for " + url);
reject(new Error("No response provided - is the service online?"));
} else if (res.statusCode !== 200 && res.statusCode !== 202) {
LogService.error("HookshotBridge", "Got status code " + res.statusCode + " when calling " + url);
LogService.error("HookshotBridge", 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);
}
} catch (e) {
LogService.error("HookshotBridge", e);
reject(e);
}
});
});
}
}

View File

@ -0,0 +1,123 @@
import HookshotGithubBridgeRecord from "../db/models/HookshotGithubBridgeRecord";
import {
HookshotGithubAuthUrls,
HookshotGithubOrg,
HookshotGithubOrgReposDto,
HookshotGithubRepo,
HookshotGithubRoomConfig,
HookshotGithubUserInfo,
HookshotTypes
} from "./models/hookshot";
import { HookshotBridge } from "./HookshotBridge";
export class HookshotGithubBridge extends HookshotBridge {
constructor(requestingUserId: string) {
super(requestingUserId);
}
protected async getDefaultBridge(): Promise<HookshotGithubBridgeRecord> {
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
if (!bridges || bridges.length !== 1) {
throw new Error("No bridges or too many bridges found");
}
return bridges[0];
}
public async getAuthUrls(): Promise<HookshotGithubAuthUrls> {
const bridge = await this.getDefaultBridge();
return this.doProvisionRequest(bridge, "GET", `/v1/github/oauth`).then(r => ({userUrl: r['user_url'], orgUrl: r['org_url']}));
}
public async getBotUserId(): Promise<string> {
const confs = await this.getAllServiceInformation();
const conf = confs.find(c => c.eventType === HookshotTypes.Github);
return conf?.botUserId;
}
public async isBridgingEnabled(): Promise<boolean> {
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
return !!bridges && bridges.length > 0 && !!(await this.getBotUserId());
}
public async getLoggedInUserInfo(): Promise<HookshotGithubUserInfo> {
const bridge = await this.getDefaultBridge();
return this.doProvisionRequest<HookshotGithubUserInfo>(bridge, "GET", `/v1/github/account`);
}
public async getRepos(orgId: string): Promise<HookshotGithubRepo[]> {
const bridge = await this.getDefaultBridge();
const results: HookshotGithubRepo[] = [];
let more = true;
let page = 1;
let perPage = 10;
do {
const res = await this.doProvisionRequest<HookshotGithubRepo[]>(bridge, "GET", `/v1/github/orgs/${orgId}/repositories`, {
page,
perPage,
}).then(r => r['repositories']);
results.push(...res);
if (res.length < perPage) more = false;
page++;
} while(more);
return results;
}
public async getInstalledLocations(): Promise<HookshotGithubOrgReposDto[]> {
const bridge = await this.getDefaultBridge();
const orgs: HookshotGithubOrg[] = [];
let lastOrgs: HookshotGithubOrg[] = [];
let page = 1;
let perPage = 10;
do {
const res = await this.doProvisionRequest<HookshotGithubUserInfo>(bridge, "GET", `/v1/github/account`, {page, perPage});
lastOrgs = res.organisations;
page++;
orgs.push(...lastOrgs);
} while(lastOrgs.length >= perPage);
const results: HookshotGithubOrgReposDto[] = [];
for (const org of orgs) {
page = 1;
let lastRepos: HookshotGithubRepo[] = [];
const repos: HookshotGithubRepo[] = [];
let changeUrl: string;
do {
const res = await this.doProvisionRequest(bridge, "GET", `/v1/github/orgs/${org.name}/repositories`, {page, perPage});
lastRepos = res['repositories'];
changeUrl = res['changeSelectionUrl'];
page++;
repos.push(...lastRepos);
} while(lastRepos.length >= perPage);
results.push({
organization: org,
repositories: repos,
changeSelectionUrl: changeUrl,
});
}
return results;
}
public async getRoomConfigurations(inRoomId: string): Promise<HookshotGithubRoomConfig[]> {
return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Github);
}
public async bridgeRoom(roomId: string, orgId: string, repoId: string): Promise<HookshotGithubRoomConfig> {
const bridge = await this.getDefaultBridge();
const body = {
commandPrefix: "!github",
org: orgId,
repo: repoId,
};
return await this.doProvisionRequest<HookshotGithubRoomConfig>(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Github}`, null, body);
}
public async unbridgeRoom(roomId: string, connectionId: string): Promise<void> {
const bridge = await this.getDefaultBridge();
await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`);
}
}

View File

@ -0,0 +1,68 @@
import HookshotJiraBridgeRecord from "../db/models/HookshotJiraBridgeRecord";
import { HookshotJiraProject, HookshotJiraRoomConfig, HookshotJiraUserInfo, HookshotTypes } from "./models/hookshot";
import { HookshotBridge } from "./HookshotBridge";
export class HookshotJiraBridge extends HookshotBridge {
constructor(requestingUserId: string) {
super(requestingUserId);
}
protected async getDefaultBridge(): Promise<HookshotJiraBridgeRecord> {
const bridges = await HookshotJiraBridgeRecord.findAll({where: {isEnabled: true}});
if (!bridges || bridges.length !== 1) {
throw new Error("No bridges or too many bridges found");
}
return bridges[0];
}
public async getAuthUrl(): Promise<string> {
const bridge = await this.getDefaultBridge();
return this.doProvisionRequest(bridge, "GET", `/v1/jira/oauth`).then(r => r['url']);
}
public async getLoggedInUserInfo(): Promise<HookshotJiraUserInfo> {
const bridge = await this.getDefaultBridge();
return this.doProvisionRequest<HookshotJiraUserInfo>(bridge, "GET", `/v1/jira/account`);
}
public async getProjects(instanceName: string): Promise<HookshotJiraProject[]> {
const bridge = await this.getDefaultBridge();
return this.doProvisionRequest<HookshotJiraProject[]>(bridge, "GET", `/v1/jira/instances/${instanceName}/projects`);
}
public async getBotUserId(): Promise<string> {
const confs = await this.getAllServiceInformation();
const conf = confs.find(c => c.eventType === HookshotTypes.Jira);
return conf?.botUserId;
}
public async isBridgingEnabled(): Promise<boolean> {
const bridges = await HookshotJiraBridgeRecord.findAll({where: {isEnabled: true}});
return !!bridges && bridges.length > 0 && !!(await this.getBotUserId());
}
public async getRoomConfigurations(inRoomId: string): Promise<HookshotJiraRoomConfig[]> {
return (await this.getAllRoomConfigurations(inRoomId))
.filter(c => c.eventType === HookshotTypes.Jira);
}
public async bridgeRoom(roomId: string, instanceName: string, projectKey: string): Promise<HookshotJiraRoomConfig> {
const bridge = await this.getDefaultBridge();
const projects = await this.getProjects(instanceName);
const project = projects.find(p => p.key === projectKey);
if (!project) throw new Error("Could not find project");
const body = {
url: project.url,
commandPrefix: "!jira",
};
return await this.doProvisionRequest<HookshotJiraRoomConfig>(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Jira}`, null, body);
}
public async unbridgeRoom(roomId: string, connectionId: string): Promise<void> {
const bridge = await this.getDefaultBridge();
await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${encodeURIComponent(connectionId)}`);
}
}

View File

@ -0,0 +1,47 @@
import { HookshotTypes, HookshotWebhookRoomConfig } from "./models/hookshot";
import { HookshotBridge } from "./HookshotBridge";
import HookshotWebhookBridgeRecord from "../db/models/HookshotWebhookBridgeRecord";
export class HookshotWebhookBridge extends HookshotBridge {
constructor(requestingUserId: string) {
super(requestingUserId);
}
protected async getDefaultBridge(): Promise<HookshotWebhookBridgeRecord> {
const bridges = await HookshotWebhookBridgeRecord.findAll({where: {isEnabled: true}});
if (!bridges || bridges.length !== 1) {
throw new Error("No bridges or too many bridges found");
}
return bridges[0];
}
public async getBotUserId(): Promise<string> {
const confs = await this.getAllServiceInformation();
const conf = confs.find(c => c.eventType === HookshotTypes.Webhook);
return conf?.botUserId;
}
public async isBridgingEnabled(): Promise<boolean> {
const bridges = await HookshotWebhookBridgeRecord.findAll({where: {isEnabled: true}});
return !!bridges && bridges.length > 0 && !!(await this.getBotUserId());
}
public async getRoomConfigurations(inRoomId: string): Promise<HookshotWebhookRoomConfig[]> {
return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Webhook);
}
public async newConnection(roomId: string, name: string): Promise<HookshotWebhookRoomConfig> {
const bridge = await this.getDefaultBridge();
const body = {
name,
};
return await this.doProvisionRequest<HookshotWebhookRoomConfig>(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Webhook}`, null, body);
}
public async removeConnection(roomId: string, connectionId: string): Promise<void> {
const bridge = await this.getDefaultBridge();
await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`);
}
}

View File

@ -0,0 +1,113 @@
export enum HookshotTypes {
Github = "uk.half-shot.matrix-hookshot.github.repository",
Jira = "uk.half-shot.matrix-hookshot.jira.project",
Webhook = "uk.half-shot.matrix-hookshot.generic.hook",
}
export interface HookshotConnection {
type: string;
eventType: string; // state key in the connection
id: string;
service: string; // human-readable
botUserId: string;
config: any; // context-specific
}
export type HookshotConnectionsResponse = HookshotConnection[];
export interface HookshotConnectionTypeDefinition {
type: string; // name of connection
eventType: string; // state key in the connection
service: string; // human-readable
botUserId: string;
}
export interface HookshotGithubRoomConfig extends HookshotConnection {
config: {
org: string;
repo: string;
ignoreHooks: SupportedGithubRepoEventType[];
commandPrefix: string;
};
}
export interface HookshotGithubOrg {
name: string;
avatarUrl: string;
}
export interface HookshotGithubOrgReposResponse {
repositories: HookshotGithubRepo[];
changeSelectionUrl?: string;
}
export interface HookshotGithubRepo {
name: string;
owner: string;
fullName: string;
avatarUrl: string;
description: string;
}
export interface HookshotGithubOrgReposDto {
organization: HookshotGithubOrg;
repositories: HookshotGithubRepo[];
changeSelectionUrl?: string;
}
export interface HookshotGithubUserInfo {
loggedIn: boolean;
organisations?: HookshotGithubOrg[];
}
export interface HookshotGithubAuthUrls {
userUrl: string;
orgUrl: string;
}
export enum SupportedGithubRepoEventType {
IssueCreated = "issue.created",
IssueChanged = "issue.changed",
IssueEdited = "issue.edited",
Issue = "issue",
PROpened = "pull_request.opened",
PRClosed = "pull_request.closed",
PRMerged = "pull_request.merged",
PRReadyForReview = "pull_request.ready_for_review",
PRReviewed = "pull_request.reviewed",
PR = "pull_request",
ReleaseCreated = "release.created",
Release = "release",
}
export interface HookshotJiraRoomConfig extends HookshotConnection {
config: {
url: string;
events: SupportedJiraEventType[];
commandPrefix: string;
};
}
export enum SupportedJiraEventType {
IssueCreated = "issue.created",
}
export interface HookshotJiraUserInfo {
loggedIn: boolean;
instances?: HookshotJiraInstance[];
}
export interface HookshotJiraInstance {
name: string;
url: string;
}
export interface HookshotJiraProject {
key: string;
name: string;
url: string;
}
export interface HookshotWebhookRoomConfig extends HookshotConnection {
config: {};
}

View File

@ -1,5 +1,8 @@
import {
Bridge,
HookshotGithubBridgeConfiguration,
HookshotJiraBridgeConfiguration,
HookshotWebhookBridgeConfiguration,
SlackBridgeConfiguration,
TelegramBridgeConfiguration,
WebhookBridgeConfiguration
@ -10,6 +13,9 @@ import { LogService } from "matrix-bot-sdk";
import { TelegramBridge } from "../bridges/TelegramBridge";
import { WebhooksBridge } from "../bridges/WebhooksBridge";
import { SlackBridge } from "../bridges/SlackBridge";
import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge";
import { HookshotJiraBridge } from "../bridges/HookshotJiraBridge";
import { HookshotWebhookBridge } from "../bridges/HookshotWebhookBridge";
export class BridgeStore {
@ -59,7 +65,7 @@ export class BridgeStore {
const record = await BridgeRecord.findOne({where: {type: integrationType}});
if (!record) throw new Error("Bridge not found");
const hasDedicatedApi = ["irc", "telegram", "webhooks", "slack"];
const hasDedicatedApi = ["irc", "telegram", "webhooks", "slack", "hookshot_github", "hookshot_jira"];
if (hasDedicatedApi.indexOf(integrationType) !== -1) {
throw new Error("This bridge should be modified with the dedicated API");
} else throw new Error("Unsupported bridge");
@ -78,6 +84,15 @@ export class BridgeStore {
} else if (record.type === "slack") {
const slack = new SlackBridge(requestingUserId);
return slack.isBridgingEnabled();
} else if (record.type === "hookshot_github") {
const hookshot = new HookshotGithubBridge(requestingUserId);
return hookshot.isBridgingEnabled();
} else if (record.type === "hookshot_jira") {
const hookshot = new HookshotJiraBridge(requestingUserId);
return hookshot.isBridgingEnabled();
} else if (record.type === "hookshot_webhook") {
const hookshot = new HookshotWebhookBridge(requestingUserId);
return hookshot.isBridgingEnabled();
} else return true;
}
@ -94,6 +109,15 @@ export class BridgeStore {
} else if (record.type === "slack") {
const slack = new SlackBridge(requestingUserId);
return slack.isBridgingEnabled();
} else if (record.type === "hookshot_github") {
const hookshot = new HookshotGithubBridge(requestingUserId);
return hookshot.isBridgingEnabled();
} else if (record.type === "hookshot_jira") {
const hookshot = new HookshotJiraBridge(requestingUserId);
return hookshot.isBridgingEnabled();
} else if (record.type === "hookshot_webhook") {
const hookshot = new HookshotWebhookBridge(requestingUserId);
return hookshot.isBridgingEnabled();
} else return false;
}
@ -131,6 +155,36 @@ export class BridgeStore {
link: link,
botUserId: info.botUserId,
};
} else if (record.type === "hookshot_github") {
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
const hookshot = new HookshotGithubBridge(requestingUserId);
const botUserId = await hookshot.getBotUserId();
const connections = await hookshot.getRoomConfigurations(inRoomId);
return <HookshotGithubBridgeConfiguration>{
botUserId: botUserId,
connections: connections,
};
} else if (record.type === "hookshot_jira") {
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
const hookshot = new HookshotJiraBridge(requestingUserId);
const botUserId = await hookshot.getBotUserId();
const connections = await hookshot.getRoomConfigurations(inRoomId);
const userInfo = await hookshot.getLoggedInUserInfo();
return <HookshotJiraBridgeConfiguration>{
botUserId: botUserId,
connections: connections,
loggedIn: userInfo.loggedIn,
instances: userInfo.instances,
};
} else if (record.type === "hookshot_webhook") {
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
const hookshot = new HookshotWebhookBridge(requestingUserId);
const botUserId = await hookshot.getBotUserId();
const connections = await hookshot.getRoomConfigurations(inRoomId);
return <HookshotWebhookBridgeConfiguration>{
botUserId: botUserId,
connections: connections,
};
} else return {};
}

View File

@ -29,6 +29,9 @@ import TermsRecord from "./models/TermsRecord";
import TermsTextRecord from "./models/TermsTextRecord";
import TermsSignedRecord from "./models/TermsSignedRecord";
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord";
import HookshotJiraBridgeRecord from "./models/HookshotJiraBridgeRecord";
import HookshotWebhookBridgeRecord from "./models/HookshotWebhookBridgeRecord";
class _DimensionStore {
private sequelize: Sequelize;
@ -75,6 +78,9 @@ class _DimensionStore {
TermsTextRecord,
TermsSignedRecord,
TermsUpstreamRecord,
HookshotGithubBridgeRecord,
HookshotJiraBridgeRecord,
HookshotWebhookBridgeRecord,
]);
}

View File

@ -13,7 +13,7 @@ export default {
{
type: "gitter",
name: "Gitter Bridge",
avatarUrl: "/img/avatars/gitter.png",
avatarUrl: "/assets/img/avatars/gitter.png",
isEnabled: true,
isPublic: true,
description: "Bridges Gitter rooms to Matrix",

View File

@ -0,0 +1,23 @@
import { QueryInterface } from "sequelize";
import { DataType } from "sequelize-typescript";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.createTable("dimension_hookshot_github_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},
"isEnabled": {type: DataType.BOOLEAN, allowNull: false},
}));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.dropTable("dimension_hookshot_github_bridges"));
}
}

View File

@ -0,0 +1,23 @@
import { QueryInterface } from "sequelize";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkInsert("dimension_bridges", [
{
type: "hookshot_github",
name: "Github Bridge",
avatarUrl: "/assets/img/avatars/github.png",
isEnabled: true,
isPublic: true,
description: "Bridges Github issues to Matrix",
},
]));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkDelete("dimension_bridges", {
type: "hookshot_github",
}));
}
}

View File

@ -0,0 +1,23 @@
import { QueryInterface } from "sequelize";
import { DataType } from "sequelize-typescript";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.createTable("dimension_hookshot_jira_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},
"isEnabled": {type: DataType.BOOLEAN, allowNull: false},
}));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.dropTable("dimension_hookshot_jira_bridges"));
}
}

View File

@ -0,0 +1,23 @@
import { QueryInterface } from "sequelize";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkInsert("dimension_bridges", [
{
type: "hookshot_jira",
name: "Jira Bridge",
avatarUrl: "/assets/img/avatars/jira.png",
isEnabled: true,
isPublic: true,
description: "Bridges Jira issues to Matrix",
},
]));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkDelete("dimension_bridges", {
type: "hookshot_jira",
}));
}
}

View File

@ -0,0 +1,23 @@
import { QueryInterface } from "sequelize";
import { DataType } from "sequelize-typescript";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.createTable("dimension_hookshot_webhook_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},
"isEnabled": {type: DataType.BOOLEAN, allowNull: false},
}));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.dropTable("dimension_hookshot_webhook_bridges"));
}
}

View File

@ -0,0 +1,23 @@
import { QueryInterface } from "sequelize";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkInsert("dimension_bridges", [
{
type: "hookshot_webhook",
name: "Webhooks Bridge",
avatarUrl: "/assets/img/avatars/webhooks.png",
isEnabled: true,
isPublic: true,
description: "Webhooks to Matrix",
},
]));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkDelete("dimension_bridges", {
type: "hookshot_webhook",
}));
}
}

View File

@ -0,0 +1,16 @@
import { QueryInterface } from "sequelize";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkUpdate("dimension_bridges", {
name: "Webhooks Bridge",
}, { type: "webhooks" }));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkUpdate("dimension_bridges", {
name: "Webhook Bridge",
}, { type: "webhooks" }));
}
}

View File

@ -0,0 +1,31 @@
import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
import Upstream from "./Upstream";
import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord";
@Table({
tableName: "dimension_hookshot_github_bridges",
underscored: false,
timestamps: false,
})
export default class HookshotGithubBridgeRecord extends Model implements IHookshotBridgeRecord {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@AllowNull
@Column
@ForeignKey(() => Upstream)
upstreamId?: number;
@AllowNull
@Column
provisionUrl?: string;
@AllowNull
@Column
sharedSecret?: string;
@Column
isEnabled: boolean;
}

View File

@ -0,0 +1,31 @@
import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
import Upstream from "./Upstream";
import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord";
@Table({
tableName: "dimension_hookshot_jira_bridges",
underscored: false,
timestamps: false,
})
export default class HookshotJiraBridgeRecord extends Model implements IHookshotBridgeRecord {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@AllowNull
@Column
@ForeignKey(() => Upstream)
upstreamId?: number;
@AllowNull
@Column
provisionUrl?: string;
@AllowNull
@Column
sharedSecret?: string;
@Column
isEnabled: boolean;
}

View File

@ -0,0 +1,31 @@
import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
import Upstream from "./Upstream";
import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord";
@Table({
tableName: "dimension_hookshot_webhook_bridges",
underscored: false,
timestamps: false,
})
export default class HookshotWebhookBridgeRecord extends Model implements IHookshotBridgeRecord {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@AllowNull
@Column
@ForeignKey(() => Upstream)
upstreamId?: number;
@AllowNull
@Column
provisionUrl?: string;
@AllowNull
@Column
sharedSecret?: string;
@Column
isEnabled: boolean;
}

View File

@ -0,0 +1,6 @@
export interface IHookshotBridgeRecord {
upstreamId?: number;
provisionUrl?: string;
sharedSecret?: string;
isEnabled: boolean;
}

View File

@ -1,4 +1,4 @@
import { LogLevel, LogService } from "matrix-bot-sdk";
import { LogLevel, LogService, RichConsoleLogger } from "matrix-bot-sdk";
import { DimensionStore } from "./db/DimensionStore";
import Webserver from "./api/Webserver";
import { CURRENT_VERSION } from "./version";
@ -16,11 +16,12 @@ declare global {
}
LogService.setLevel(LogLevel.DEBUG);
LogService.setLogger(new RichConsoleLogger());
LogService.info("index", "Starting dimension " + CURRENT_VERSION);
async function startup() {
const schemas = await DimensionStore.updateSchema();
LogService.info("DimensionStore", schemas);
LogService.info("DimensionStore", "Applied schemas: ", schemas);
const webserver = new Webserver();
await webserver.start();

View File

@ -4,6 +4,12 @@ import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge";
import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge";
import { WebhookConfiguration } from "../bridges/models/webhooks";
import { BridgedChannel } from "../bridges/SlackBridge";
import {
HookshotGithubRoomConfig,
HookshotJiraInstance,
HookshotJiraRoomConfig,
HookshotWebhookRoomConfig
} from "../bridges/models/hookshot";
const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks"];
@ -45,3 +51,20 @@ export interface SlackBridgeConfiguration {
link: BridgedChannel;
botUserId: string;
}
export interface HookshotGithubBridgeConfiguration {
botUserId: string;
connections: HookshotGithubRoomConfig[];
}
export interface HookshotJiraBridgeConfiguration {
botUserId: string;
connections: HookshotJiraRoomConfig[];
loggedIn: boolean;
instances?: HookshotJiraInstance[];
}
export interface HookshotWebhookBridgeConfiguration {
botUserId: string;
connections: HookshotWebhookRoomConfig[];
}

View File

@ -2,8 +2,8 @@
@include themifyComponent() {
.adminNav {
margin: 32px 0 0;
width: 200px;
margin: 0;
padding: 0;
position: fixed;
top: 100px;
@ -34,8 +34,9 @@
}
.adminContent {
margin-top: 32px;
position: fixed;
top: 120px;
top: 85px;
left: 200px;
bottom: 0;
right: 0;
@ -53,4 +54,4 @@
font-size: 11px;
font-family: monospace;
}
}
}

View File

@ -0,0 +1,41 @@
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading">
<my-ibox boxTitle="Github Bridge Configurations">
<div class="my-ibox-content">
<p>
<a href="https://github.com/half-shot/matrix-hookshot" target="_blank">{{'matrix-hookshot' | translate}}</a>
{{'is a multi-purpose bridge which supports Github as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.' | translate}}
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>{{'Name' | translate}}</th>
<th class="text-center" style="width: 120px;">{{'Actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngIf="!configurations || configurations.length === 0">
<td colspan="3"><i>{{'No bridge configurations.' | translate}}</i></td>
</tr>
<tr *ngFor="let bridge of configurations trackById">
<td>
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
<span class="text-muted" style="display: inline-block;"
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
</td>
<td class="text-center">
<span class="editButton" (click)="editBridge(bridge)">
<i class="fa fa-pencil-alt"></i>
</span>
</td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
[disabled]="configurations && configurations.length > 0">
<i class="fa fa-plus"></i> {{'Add self-hosted bridge' | translate}}
</button>
</div>
</my-ibox>
</div>

View File

@ -0,0 +1,3 @@
.editButton {
cursor: pointer;
}

View File

@ -0,0 +1,85 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import {
AdminHookshotGithubBridgeManageSelfhostedComponent,
ManageSelfhostedHookshotGithubBridgeDialogContext
} from "./manage-selfhosted/manage-selfhosted.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
import { AdminHookshotGithubApiService } from "../../../shared/services/admin/admin-hookshot-github-api.service";
import { FE_HookshotGithubBridge } from "../../../shared/models/hookshot_github";
@Component({
templateUrl: "./hookshot-github.component.html",
styleUrls: ["./hookshot-github.component.scss"],
})
export class AdminHookshotGithubBridgeComponent implements OnInit {
public isLoading = true;
public isUpdating = false;
public configurations: FE_HookshotGithubBridge[] = [];
constructor(private hookshotApi: AdminHookshotGithubApiService,
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
public ngOnInit() {
this.reload().then(() => this.isLoading = false);
}
private async reload(): Promise<any> {
try {
this.configurations = await this.hookshotApi.getBridges();
} catch (err) {
console.error(err);
this.translate.get('Error loading bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
}
public addSelfHostedBridge() {
const selfhostedRef = this.modal.open(AdminHookshotGithubBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated Github bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotGithubBridgeDialogContext;
selfhostedInstance.provisionUrl = '';
selfhostedInstance.sharedSecret = '';
}
public editBridge(bridge: FE_HookshotGithubBridge) {
const selfhostedRef = this.modal.open(AdminHookshotGithubBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated Github bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotGithubBridgeDialogContext;
selfhostedInstance.provisionUrl = bridge.provisionUrl;
selfhostedInstance.sharedSecret = bridge.sharedSecret;
selfhostedInstance.bridgeId = bridge.id;
selfhostedInstance.isAdding = !bridge.id;
}
}

View File

@ -0,0 +1,31 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'self-hosted Github bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>{{'Self-hosted Github bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9000"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Shared Secret' | translate}}
<span class="text-muted ">{{'The shared secret defined in the configuration for provisioning.' | translate}}</span>
<input type="text" class="form-control"
placeholder="some_secret_value"
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -0,0 +1,63 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { TranslateService } from "@ngx-translate/core";
import { AdminHookshotGithubApiService } from "../../../../shared/services/admin/admin-hookshot-github-api.service";
export interface ManageSelfhostedHookshotGithubBridgeDialogContext {
provisionUrl: string;
sharedSecret: string;
bridgeId: number;
isAdding: boolean;
}
@Component({
templateUrl: "./manage-selfhosted.component.html",
styleUrls: ["./manage-selfhosted.component.scss"],
})
export class AdminHookshotGithubBridgeManageSelfhostedComponent {
isSaving = false;
provisionUrl: string;
sharedSecret: string;
bridgeId: number;
isAdding = true;
constructor(public modal: NgbActiveModal,
private hookshotApi: AdminHookshotGithubApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
}
public add() {
this.isSaving = true;
if (this.isAdding) {
this.hookshotApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Github bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to create Github bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else {
this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Github bridge updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to update Github bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}
}

View File

@ -0,0 +1,41 @@
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading">
<my-ibox boxTitle="Jira Bridge Configurations">
<div class="my-ibox-content">
<p>
<a href="https://github.com/half-shot/matrix-hookshot" target="_blank">{{'matrix-hookshot' | translate}}</a>
{{'is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.' | translate}}
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>{{'Name' | translate}}</th>
<th class="text-center" style="width: 120px;">{{'Actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngIf="!configurations || configurations.length === 0">
<td colspan="3"><i>{{'No bridge configurations.' | translate}}</i></td>
</tr>
<tr *ngFor="let bridge of configurations trackById">
<td>
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
<span class="text-muted" style="display: inline-block;"
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
</td>
<td class="text-center">
<span class="editButton" (click)="editBridge(bridge)">
<i class="fa fa-pencil-alt"></i>
</span>
</td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
[disabled]="configurations && configurations.length > 0">
<i class="fa fa-plus"></i> {{'Add self-hosted bridge' | translate}}
</button>
</div>
</my-ibox>
</div>

View File

@ -0,0 +1,3 @@
.editButton {
cursor: pointer;
}

View File

@ -0,0 +1,85 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import {
AdminHookshotJiraBridgeManageSelfhostedComponent,
ManageSelfhostedHookshotJiraBridgeDialogContext
} from "./manage-selfhosted/manage-selfhosted.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
import { FE_HookshotJiraBridge } from "../../../shared/models/hookshot_jira";
import { AdminHookshotJiraApiService } from "../../../shared/services/admin/admin-hookshot-jira-api.service";
@Component({
templateUrl: "./hookshot-jira.component.html",
styleUrls: ["./hookshot-jira.component.scss"],
})
export class AdminHookshotJiraBridgeComponent implements OnInit {
public isLoading = true;
public isUpdating = false;
public configurations: FE_HookshotJiraBridge[] = [];
constructor(private hookshotApi: AdminHookshotJiraApiService,
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
public ngOnInit() {
this.reload().then(() => this.isLoading = false);
}
private async reload(): Promise<any> {
try {
this.configurations = await this.hookshotApi.getBridges();
} catch (err) {
console.error(err);
this.translate.get('Error loading bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
}
public addSelfHostedBridge() {
const selfhostedRef = this.modal.open(AdminHookshotJiraBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated Jira bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotJiraBridgeDialogContext;
selfhostedInstance.provisionUrl = '';
selfhostedInstance.sharedSecret = '';
}
public editBridge(bridge: FE_HookshotJiraBridge) {
const selfhostedRef = this.modal.open(AdminHookshotJiraBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated Jira bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotJiraBridgeDialogContext;
selfhostedInstance.provisionUrl = bridge.provisionUrl;
selfhostedInstance.sharedSecret = bridge.sharedSecret;
selfhostedInstance.bridgeId = bridge.id;
selfhostedInstance.isAdding = !bridge.id;
}
}

View File

@ -0,0 +1,31 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'self-hosted Github bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>{{'Self-hosted Jira bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9000"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Shared Secret' | translate}}
<span class="text-muted ">{{'The shared secret defined in the configuration for provisioning.' | translate}}</span>
<input type="text" class="form-control"
placeholder="some_secret_value"
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -0,0 +1,63 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { TranslateService } from "@ngx-translate/core";
import { AdminHookshotJiraApiService } from "../../../../shared/services/admin/admin-hookshot-jira-api.service";
export interface ManageSelfhostedHookshotJiraBridgeDialogContext {
provisionUrl: string;
sharedSecret: string;
bridgeId: number;
isAdding: boolean;
}
@Component({
templateUrl: "./manage-selfhosted.component.html",
styleUrls: ["./manage-selfhosted.component.scss"],
})
export class AdminHookshotJiraBridgeManageSelfhostedComponent {
isSaving = false;
provisionUrl: string;
sharedSecret: string;
bridgeId: number;
isAdding = true;
constructor(public modal: NgbActiveModal,
private hookshotApi: AdminHookshotJiraApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
}
public add() {
this.isSaving = true;
if (this.isAdding) {
this.hookshotApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Jira bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to create Jira bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else {
this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Jira bridge updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to update Jira bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}
}

View File

@ -0,0 +1,41 @@
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading">
<my-ibox boxTitle="Webhook Bridge Configurations">
<div class="my-ibox-content">
<p>
<a href="https://github.com/half-shot/matrix-hookshot" target="_blank">{{'matrix-hookshot' | translate}}</a>
{{'is a multi-purpose bridge which supports Generic Webhooks as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a webhook into the room.' | translate}}
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>{{'Name' | translate}}</th>
<th class="text-center" style="width: 120px;">{{'Actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngIf="!configurations || configurations.length === 0">
<td colspan="3"><i>{{'No bridge configurations.' | translate}}</i></td>
</tr>
<tr *ngFor="let bridge of configurations trackById">
<td>
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
<span class="text-muted" style="display: inline-block;"
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
</td>
<td class="text-center">
<span class="editButton" (click)="editBridge(bridge)">
<i class="fa fa-pencil-alt"></i>
</span>
</td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
[disabled]="configurations && configurations.length > 0">
<i class="fa fa-plus"></i> {{'Add self-hosted bridge' | translate}}
</button>
</div>
</my-ibox>
</div>

View File

@ -0,0 +1,3 @@
.editButton {
cursor: pointer;
}

View File

@ -0,0 +1,85 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import {
AdminHookshotWebhookBridgeManageSelfhostedComponent,
ManageSelfhostedHookshotWebhookBridgeDialogContext
} from "./manage-selfhosted/manage-selfhosted.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
import { AdminHookshotWebhookApiService } from "../../../shared/services/admin/admin-hookshot-webhook-api.service";
import { FE_HookshotWebhookBridge } from "../../../shared/models/hookshot_webhook";
@Component({
templateUrl: "./hookshot-webhook.component.html",
styleUrls: ["./hookshot-webhook.component.scss"],
})
export class AdminHookshotWebhookBridgeComponent implements OnInit {
public isLoading = true;
public isUpdating = false;
public configurations: FE_HookshotWebhookBridge[] = [];
constructor(private hookshotApi: AdminHookshotWebhookApiService,
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
public ngOnInit() {
this.reload().then(() => this.isLoading = false);
}
private async reload(): Promise<any> {
try {
this.configurations = await this.hookshotApi.getBridges();
} catch (err) {
console.error(err);
this.translate.get('Error loading bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
}
public addSelfHostedBridge() {
const selfhostedRef = this.modal.open(AdminHookshotWebhookBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated Webhook bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotWebhookBridgeDialogContext;
selfhostedInstance.provisionUrl = '';
selfhostedInstance.sharedSecret = '';
}
public editBridge(bridge: FE_HookshotWebhookBridge) {
const selfhostedRef = this.modal.open(AdminHookshotWebhookBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated Webhook bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotWebhookBridgeDialogContext;
selfhostedInstance.provisionUrl = bridge.provisionUrl;
selfhostedInstance.sharedSecret = bridge.sharedSecret;
selfhostedInstance.bridgeId = bridge.id;
selfhostedInstance.isAdding = !bridge.id;
}
}

View File

@ -0,0 +1,31 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'self-hosted Github bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>{{'Self-hosted Webhook bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9000"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Shared Secret' | translate}}
<span class="text-muted ">{{'The shared secret defined in the configuration for provisioning.' | translate}}</span>
<input type="text" class="form-control"
placeholder="some_secret_value"
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -0,0 +1,63 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { TranslateService } from "@ngx-translate/core";
import { AdminHookshotWebhookApiService } from "../../../../shared/services/admin/admin-hookshot-webhook-api.service";
export interface ManageSelfhostedHookshotWebhookBridgeDialogContext {
provisionUrl: string;
sharedSecret: string;
bridgeId: number;
isAdding: boolean;
}
@Component({
templateUrl: "./manage-selfhosted.component.html",
styleUrls: ["./manage-selfhosted.component.scss"],
})
export class AdminHookshotWebhookBridgeManageSelfhostedComponent {
isSaving = false;
provisionUrl: string;
sharedSecret: string;
bridgeId: number;
isAdding = true;
constructor(public modal: NgbActiveModal,
private hookshotApi: AdminHookshotWebhookApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
}
public add() {
this.isSaving = true;
if (this.isAdding) {
this.hookshotApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Webhook bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to create Webhook bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else {
this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Webhook bridge updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to update Webhook bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}
}

View File

@ -5,9 +5,7 @@ import { FE_Upstream } from "../../../shared/models/admin-responses";
import { AdminUpstreamApiService } from "../../../shared/services/admin/admin-upstream-api.service";
import { FE_IrcBridge } from "../../../shared/models/irc";
import { AdminIrcBridgeNetworksComponent, IrcNetworksDialogContext } from "./networks/networks.component";
import {
AdminIrcBridgeAddSelfhostedComponent
} from "./add-selfhosted/add-selfhosted.component";
import { AdminIrcBridgeAddSelfhostedComponent } from "./add-selfhosted/add-selfhosted.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";

View File

@ -4,5 +4,5 @@ tr td:last-child {
.editButton {
cursor: pointer;
vertical-align: text-bottom;
}
vertical-align: middle;
}

View File

@ -2,9 +2,7 @@ import { Component } from "@angular/core";
import { AdminApiService } from "../../shared/services/admin/admin-api.service";
import { FE_DimensionConfig } from "../../shared/models/admin-responses";
import { ToasterService } from "angular2-toaster";
import {
AdminLogoutConfirmationDialogComponent,
} from "./logout-confirmation/logout-confirmation.component";
import { AdminLogoutConfirmationDialogComponent, } from "./logout-confirmation/logout-confirmation.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";

View File

@ -4,12 +4,12 @@ tr td:last-child {
.previewButton {
cursor: pointer;
vertical-align: text-bottom;
vertical-align: middle;
}
.removeButton {
cursor: pointer;
vertical-align: text-bottom;
vertical-align: middle;
}
.telegram-import {

View File

@ -4,9 +4,7 @@ import { AdminTermsApiService } from "../../../shared/services/admin/admin-terms
import { ActivatedRoute, Router } from "@angular/router";
import * as ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import ISO6391 from "iso-639-1";
import {
AdminTermsNewEditPublishDialogComponent,
} from "./publish/publish.component";
import { AdminTermsNewEditPublishDialogComponent, } from "./publish/publish.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";

View File

@ -4,6 +4,6 @@
}
main {
font-family: 'Roboto', 'Open Sans', Arial, sans-serif;
font-family: 'Inter', Arial, sans-serif;
display: block;
}

View File

@ -11,7 +11,7 @@ import { HttpClient } from "@angular/common/http";
export class AppComponent {
constructor(public translate: TranslateService, public http: HttpClient) {
translate.addLangs(["en", "de"]);
translate.setDefaultLang("de");
translate.setDefaultLang("en");
if (navigator.language === "de") {
translate.use("de");
} else {

View File

@ -10,10 +10,10 @@ import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from "./app.component";
import { HomeComponent } from "./home/home.component";
import { createNewHosts, removeNgStyles } from "@angularclass/hmr";
import { RiotComponent } from "./riot/riot.component";
import { ElementComponent } from "./element/element.component";
import { ScalarClientApiService } from "./shared/services/scalar/scalar-client-api.service";
import { ToasterModule, ToasterService } from "angular2-toaster";
import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component";
import { ScalarCloseComponent } from "./element/scalar-close/scalar-close.component";
import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component";
import { ToggleFullscreenDirective } from "./shared/directives/toggle-fullscreen.directive";
import { FullscreenButtonComponent } from "./elements/fullscreen-button/fullscreen-button.component";
@ -22,7 +22,7 @@ import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.compo
import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component";
import { PageHeaderComponent } from "./page-header/page-header.component";
import { SpinnerComponent } from "./elements/spinner/spinner.component";
import { RiotHomeComponent } from "./riot/riot-home/home.component";
import { ElementHomeComponent } from "./element/element-home/home.component";
import { IntegrationBagComponent } from "./integration-bag/integration-bag.component";
import { ScalarServerApiService } from "./shared/services/scalar/scalar-server-api.service";
import { AdminApiService } from "./shared/services/admin/admin-api.service";
@ -56,8 +56,9 @@ import { AdminNebGiphyConfigComponent } from "./admin/neb/config/giphy/giphy.com
import { AdminNebGuggyConfigComponent } from "./admin/neb/config/guggy/guggy.component";
import { AdminNebGoogleConfigComponent } from "./admin/neb/config/google/google.component";
import { AdminNebImgurConfigComponent } from "./admin/neb/config/imgur/imgur.component";
import { ConfigSimpleBotComponent } from "./configs/simple-bot/simple-bot.component";
import { ConfigScreenComplexBotComponent } from "./configs/complex-bot/config-screen/config-screen.complex-bot.component";
import {
ConfigScreenComplexBotComponent
} from "./configs/complex-bot/config-screen/config-screen.complex-bot.component";
import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component";
import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component";
import { ConfigScreenBridgeComponent } from "./configs/bridge/config-screen/config-screen.bridge.component";
@ -78,17 +79,23 @@ import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.co
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";
import {
AdminTelegramBridgeManageSelfhostedComponent
} from "./admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component";
import { TelegramApiService } from "./shared/services/integrations/telegram-api.service";
import { TelegramBridgeConfigComponent } from "./configs/bridge/telegram/telegram.bridge.component";
import { TelegramAskUnbridgeComponent } from "./configs/bridge/telegram/ask-unbridge/ask-unbridge.component";
import { TelegramCannotUnbridgeComponent } from "./configs/bridge/telegram/cannot-unbridge/cannot-unbridge.component";
import { AdminWebhooksBridgeManageSelfhostedComponent } from "./admin/bridges/webhooks/manage-selfhosted/manage-selfhosted.component";
import {
AdminWebhooksBridgeManageSelfhostedComponent
} from "./admin/bridges/webhooks/manage-selfhosted/manage-selfhosted.component";
import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks.component";
import { AdminWebhooksApiService } from "./shared/services/admin/admin-webhooks-api.service";
import { WebhooksApiService } from "./shared/services/integrations/webhooks-api.service";
import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component";
import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component";
import {
GenericFullscreenWidgetWrapperComponent
} from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component";
import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component";
import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component";
import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component";
@ -99,7 +106,9 @@ import { AdminCustomBotsComponent } from "./admin/custom-bots/custom-bots.compon
import { AdminAddCustomBotComponent } from "./admin/custom-bots/add/add.component";
import { SlackApiService } from "./shared/services/integrations/slack-api.service";
import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge.component";
import { AdminSlackBridgeManageSelfhostedComponent } from "./admin/bridges/slack/manage-selfhosted/manage-selfhosted.component";
import {
AdminSlackBridgeManageSelfhostedComponent
} from "./admin/bridges/slack/manage-selfhosted/manage-selfhosted.component";
import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component";
import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.service";
import { AdminLogoutConfirmationDialogComponent } from "./admin/home/logout-confirmation/logout-confirmation.component";
@ -119,6 +128,30 @@ import { AdminWidgetWhiteboardConfigComponent } from "./admin/widgets/whiteboard
import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-github/hookshot-github.component";
import {
AdminHookshotGithubBridgeManageSelfhostedComponent
} from "./admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component";
import { AdminHookshotGithubApiService } from "./shared/services/admin/admin-hookshot-github-api.service";
import { HookshotGithubApiService } from "./shared/services/integrations/hookshot-github-api.service";
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component";
import {
AdminHookshotJiraBridgeManageSelfhostedComponent
} from "./admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component";
import { AdminHookshotJiraApiService } from "./shared/services/admin/admin-hookshot-jira-api.service";
import { HookshotJiraApiService } from "./shared/services/integrations/hookshot-jira-api.service";
import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component";
import { AdminHookshotWebhookBridgeComponent } from "./admin/bridges/hookshot-webhook/hookshot-webhook.component";
import {
AdminHookshotWebhookBridgeManageSelfhostedComponent
} from "./admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component";
import { AdminHookshotWebhookApiService } from "./shared/services/admin/admin-hookshot-webhook-api.service";
import { HookshotWebhookApiService } from "./shared/services/integrations/hookshot-webhook-api.service";
import {
HookshotWebhookBridgeConfigComponent
} from "./configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component";
import { FieldComponent } from "./elements/field/field.component";
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient) {
@ -149,7 +182,7 @@ export function HttpLoaderFactory(http: HttpClient) {
declarations: [
AppComponent,
HomeComponent,
RiotComponent,
ElementComponent,
IntegrationBagComponent,
PageHeaderComponent,
SpinnerComponent,
@ -162,7 +195,7 @@ export function HttpLoaderFactory(http: HttpClient) {
BigBlueButtonWidgetWrapperComponent,
GCalWidgetWrapperComponent,
BigBlueButtonConfigComponent,
RiotHomeComponent,
ElementHomeComponent,
IboxComponent,
ConfigScreenWidgetComponent,
CustomWidgetConfigComponent,
@ -185,7 +218,6 @@ export function HttpLoaderFactory(http: HttpClient) {
AdminNebGuggyConfigComponent,
AdminNebGoogleConfigComponent,
AdminNebImgurConfigComponent,
ConfigSimpleBotComponent,
ConfigScreenComplexBotComponent,
RssComplexBotConfigComponent,
TravisCiComplexBotConfigComponent,
@ -227,7 +259,17 @@ export function HttpLoaderFactory(http: HttpClient) {
AdminTermsNewEditPublishDialogComponent,
TermsWidgetWrapperComponent,
WhiteboardWidgetComponent,
AdminWidgetWhiteboardConfigComponent
AdminWidgetWhiteboardConfigComponent,
AdminHookshotGithubBridgeComponent,
AdminHookshotGithubBridgeManageSelfhostedComponent,
HookshotGithubBridgeConfigComponent,
AdminHookshotJiraBridgeComponent,
AdminHookshotJiraBridgeManageSelfhostedComponent,
HookshotJiraBridgeConfigComponent,
AdminHookshotWebhookBridgeComponent,
AdminHookshotWebhookBridgeManageSelfhostedComponent,
HookshotWebhookBridgeConfigComponent,
FieldComponent,
// Vendor
],
@ -257,6 +299,12 @@ export function HttpLoaderFactory(http: HttpClient) {
AdminSlackApiService,
ToasterService,
AdminTermsApiService,
AdminHookshotGithubApiService,
HookshotGithubApiService,
AdminHookshotJiraApiService,
HookshotJiraApiService,
AdminHookshotWebhookApiService,
HookshotWebhookApiService,
{provide: Window, useValue: window},
// Vendor
@ -270,7 +318,6 @@ export function HttpLoaderFactory(http: HttpClient) {
AdminNebGuggyConfigComponent,
AdminNebGoogleConfigComponent,
AdminNebImgurConfigComponent,
ConfigSimpleBotComponent,
AdminIrcBridgeNetworksComponent,
AdminIrcBridgeAddSelfhostedComponent,
AdminStickerPackPreviewComponent,
@ -282,7 +329,7 @@ export function HttpLoaderFactory(http: HttpClient) {
AdminSlackBridgeManageSelfhostedComponent,
AdminLogoutConfirmationDialogComponent,
AdminTermsNewEditPublishDialogComponent,
AdminWidgetWhiteboardConfigComponent
AdminWidgetWhiteboardConfigComponent,
]
})
export class AppModule {
@ -291,7 +338,7 @@ export class AppModule {
}
hmrOnInit(store) {
console.log("HMR store", store);
console.log("Dimension HMR store", store);
}
hmrOnDestroy(store) {

View File

@ -1,13 +1,13 @@
import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { RiotComponent } from "./riot/riot.component";
import { ElementComponent } from "./element/element.component";
import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component";
import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component";
import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component";
import { VideoWidgetWrapperComponent } from "./widget-wrappers/video/video.component";
import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component";
import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component";
import { RiotHomeComponent } from "./riot/riot-home/home.component";
import { ElementHomeComponent } from "./element/element-home/home.component";
import { CustomWidgetConfigComponent } from "./configs/widget/custom/custom.widget.component";
import { EtherpadWidgetConfigComponent } from "./configs/widget/etherpad/etherpad.widget.component";
import { GoogleCalendarWidgetConfigComponent } from "./configs/widget/google-calendar/gcal.widget.component";
@ -33,7 +33,9 @@ import { AdminTelegramBridgeComponent } from "./admin/bridges/telegram/telegram.
import { TelegramBridgeConfigComponent } from "./configs/bridge/telegram/telegram.bridge.component";
import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks.component";
import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component";
import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component";
import {
GenericFullscreenWidgetWrapperComponent
} from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component";
import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component";
import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component";
import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component";
@ -48,19 +50,27 @@ import { AdminTermsComponent } from "./admin/terms/terms.component";
import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component";
import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component";
import { WhiteboardWidgetComponent } from "./configs/widget/whiteboard/whiteboard.widget.component";
import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-github/hookshot-github.component";
import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component";
import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component";
import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component";
import { AdminHookshotWebhookBridgeComponent } from "./admin/bridges/hookshot-webhook/hookshot-webhook.component";
import {
HookshotWebhookBridgeConfigComponent
} from "./configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component";
const routes: Routes = [
{path: "", component: HomeComponent},
{path: "riot", pathMatch: "full", redirectTo: "riot-app"},
{path: "element", pathMatch: "full", redirectTo: "riot-app"},
{path: "riot", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Integration manager"}},
{path: "element", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Integration manager"}},
{
path: "riot-app",
component: RiotComponent,
data: {breadcrumb: "Home", name: "Dimension"},
component: ElementComponent,
data: {breadcrumb: "Home", name: "Integration manager"},
children: [
{
path: "",
component: RiotHomeComponent,
component: ElementHomeComponent,
},
{
path: "admin",
@ -134,6 +144,21 @@ const routes: Routes = [
component: AdminSlackBridgeComponent,
data: {breadcrumb: "Slack Bridge", name: "Slack Bridge"},
},
{
path: "hookshot_github",
component: AdminHookshotGithubBridgeComponent,
data: {breadcrumb: "Github Bridge", name: "Github Bridge"},
},
{
path: "hookshot_jira",
component: AdminHookshotJiraBridgeComponent,
data: {breadcrumb: "Jira Bridge", name: "Jira Bridge"},
},
{
path: "hookshot_webhook",
component: AdminHookshotWebhookBridgeComponent,
data: {breadcrumb: "Webhook Bridge", name: "Webhook Bridge"},
},
],
},
{
@ -180,97 +205,114 @@ const routes: Routes = [
{
path: "bigbluebutton",
component: BigBlueButtonConfigComponent,
data: {breadcrumb: "BigBlueButton Widgets", name: "BigBlueButton Widgets"},
data: {breadcrumb: "BigBlueButton", name: "BigBlueButton"},
},
{
path: "etherpad",
component: EtherpadWidgetConfigComponent,
data: {breadcrumb: "Notes Widgets", name: "Notes Widgets"},
data: {breadcrumb: "Etherpad", name: "Etherpad"},
},
{
path: "googlecalendar",
component: GoogleCalendarWidgetConfigComponent,
data: {breadcrumb: "Google Calendar Widgets", name: "Google Calendar Widgets"},
data: {breadcrumb: "Google Calendar", name: "Google Calendar"},
},
{
path: "googledocs",
component: GoogleDocsWidgetConfigComponent,
data: {breadcrumb: "Google Doc Widgets", name: "Google Doc Widgets"},
data: {breadcrumb: "Google Docs", name: "Google Docs"},
},
{
path: "jitsi",
component: JitsiWidgetConfigComponent,
data: {breadcrumb: "Jitsi Widgets", name: "Jitsi Widgets"},
data: {breadcrumb: "Jitsi", name: "Jitsi"},
},
{
path: "twitch",
component: TwitchWidgetConfigComponent,
data: {breadcrumb: "Twitch Livestream Widgets", name: "Twitch Livestream Widgets"},
data: {breadcrumb: "Twitch Livestream", name: "Twitch Livestream"},
},
{
path: "youtube",
component: YoutubeWidgetConfigComponent,
data: {breadcrumb: "Youtube Video Widgets", name: "Youtube Video Widgets"},
data: {breadcrumb: "Youtube", name: "Youtube"},
},
{
path: "grafana",
component: GrafanaWidgetConfigComponent,
data: {breadcrumb: "Grafana Widgets", name: "Grafana Widgets"},
data: {breadcrumb: "Grafana", name: "Grafana"},
},
{
path: "tradingview",
component: TradingViewWidgetConfigComponent,
data: {breadcrumb: "TradingView Widgets", name: "TradingView Widgets"},
data: {breadcrumb: "TradingView", name: "TradingView"},
},
{
path: "spotify",
component: SpotifyWidgetConfigComponent,
data: {breadcrumb: "Spotify Widgets", name: "Spotify Widgets"},
data: {breadcrumb: "Spotify", name: "Spotify"},
},
{
path: "whiteboard",
component: WhiteboardWidgetComponent,
data: {breadcrumb: "Whiteboard Widgets", name: "Whiteboard Widgets"},
data: {breadcrumb: "Whiteboard", name: "Whiteboard"},
},
],
},
{
path: "complex-bot",
data: {breadcrumb: {skip: true}},
children: [
{
path: "rss",
component: RssComplexBotConfigComponent,
data: {breadcrumb: "RSS Bot Configuration", name: "RSS Bot Configuration"},
data: {breadcrumb: "RSS Bot", name: "RSS Bot"},
},
{
path: "travisci",
component: TravisCiComplexBotConfigComponent,
data: {breadcrumb: "Travis CI Configuration", name: "Travis CI Configuration"},
data: {breadcrumb: "Travis CI", name: "Travis CI"},
},
],
},
{
path: "bridge",
data: {breadcrumb: {skip: true}},
children: [
{
path: "irc",
component: IrcBridgeConfigComponent,
data: {breadcrumb: "IRC Bridge Configuration", name: "IRC Bridge Configuration"},
data: {breadcrumb: "IRC Bridge", name: "IRC Bridge"},
},
{
path: "telegram",
component: TelegramBridgeConfigComponent,
data: {breadcrumb: "Telegram Bridge Configuration", name: "Telegram Bridge Configuration"},
data: {breadcrumb: "Telegram Bridge", name: "Telegram Bridge"},
},
{
path: "webhooks",
component: WebhooksBridgeConfigComponent,
data: {breadcrumb: "Webhook Bridge Configuration", name: "Webhook Bridge Configuration"},
data: {breadcrumb: "Webhooks Bridge", name: "Webhooks Bridge"},
},
{
path: "slack",
component: SlackBridgeConfigComponent,
data: {breadcrumb: "Slack Bridge Configuration", name: "Slack Bridge Configuration"},
data: {breadcrumb: "Slack Bridge", name: "Slack Bridge"},
},
{
path: "hookshot_github",
component: HookshotGithubBridgeConfigComponent,
data: {breadcrumb: "Github Bridge", name: "Github Bridge"},
},
{
path: "hookshot_jira",
component: HookshotJiraBridgeConfigComponent,
data: {breadcrumb: "Jira Bridge", name: "Jira Bridge"},
},
{
path: "hookshot_webhook",
component: HookshotWebhookBridgeConfigComponent,
data: {breadcrumb: "Webhooks Bridge", name: "Webhooks Bridge"},
},
],
},
@ -283,6 +325,7 @@ const routes: Routes = [
},
{
path: "widgets",
data: {breadcrumb: {skip: true}},
children: [
{path: "terms/:shortcode/:lang/:version", component: TermsWidgetWrapperComponent},
{path: "generic", component: GenericWidgetWrapperComponent},

View File

@ -1,4 +1,4 @@
import { Inject, Injectable, Input, OnDestroy, OnInit } from "@angular/core";
import { Inject, Injectable, OnDestroy, OnInit } from "@angular/core";
import { FE_Bridge } from "../../shared/models/integration";
import { ActivatedRoute } from "@angular/router";
import { Subscription } from "rxjs/Subscription";

View File

@ -0,0 +1,59 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-spinner *ngIf="loadingConnections"></my-spinner>
<div *ngIf="!loadingConnections">
<div *ngIf="isBridged">
<p>{{'This room is bridged to' | translate}} {{bridgedRepoSlug}}</p>
<button type="button" class="element-btn element-btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
<div *ngIf="!isBridged && authUrl">
<p>
{{'In order to bridge to Github, you\'ll need to authorize the bridge to access your organization(s). Please click the button below to do so.' | translate}}
</p>
<a [href]="authUrl" rel="noopener" target="_blank" class="btn btn-lg btn-link">
<img src="/assets/img/avatars/github.png" width="35" /> {{'Sign in with GitHub' | translate}}
</a>
</div>
<div *ngIf="!isBridged && orgAuthUrl">
<p>
{{'Almost there! Just need to add the bot to your organizations now.' | translate}}
</p>
<a [href]="orgAuthUrl" rel="noopener" target="_blank" class="btn btn-lg btn-link">
<img src="/assets/img/avatars/github.png" width="35" /> {{'Add to GitHub' | translate}}
</a>
</div>
<div *ngIf="!isBridged && !authUrl && !orgAuthUrl">
<my-field
label="{{'Organization' | translate}}"
[asSelect]="true"
[(value)]="orgId"
[disabled]="isBusy"
(valueChange)="loadRepos()"
[selectOptions]="orgs"
></my-field>
<my-field
label="{{'Repository' | translate}}"
[asSelect]="true"
[(value)]="repoId"
[disabled]="isBusy"
[selectOptions]="repos"
></my-field>
<p *ngIf="orgAddAuthUrl">
<a [href]="orgAddAuthUrl" rel="noopener" target="_blank">
{{'Add to another GitHub organization' | translate}}
</a>
</p>
<p *ngIf="orgEditAuthUrl">
<a [href]="orgEditAuthUrl" rel="noopener" target="_blank">
{{'Add more repositories from ' | translate}} {{ orgId }}
</a>
</p>
<button type="button" class="element-btn" [disabled]="isBusy" (click)="bridgeRoom()">
Bridge
</button>
</div>
</div>
</ng-template>
</my-bridge-config>

View File

@ -0,0 +1,4 @@
.actions-col {
width: 120px;
text-align: center;
}

View File

@ -0,0 +1,176 @@
import { Component, OnInit } from "@angular/core";
import { BridgeComponent } from "../bridge.component";
import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
import { FE_HookshotGithubConnection, FE_HookshotGithubOrgReposDto } from "../../../shared/models/hookshot_github";
import { HookshotGithubApiService } from "../../../shared/services/integrations/hookshot-github-api.service";
interface HookshotConfig {
botUserId: string;
connections: FE_HookshotGithubConnection[];
}
@Component({
templateUrl: "hookshot-github.bridge.component.html",
styleUrls: ["hookshot-github.bridge.component.scss"],
})
export class HookshotGithubBridgeConfigComponent extends BridgeComponent<HookshotConfig> implements OnInit {
public isBusy: boolean;
public authUrl: SafeUrl;
public orgAuthUrl: SafeUrl;
public loadingConnections = true;
public bridgedRepoSlug: string;
public orgEditAuthUrl: SafeUrl;
public orgAddAuthUrl: SafeUrl;
public orgs: string[] = [];
public orgId: string;
public repos: string[] = [];
public repoId: string;
private timerId: any;
private orgToRepoMap: Record<string, FE_HookshotGithubOrgReposDto>;
constructor(private hookshot: HookshotGithubApiService, private scalar: ScalarClientApiService, private sanitizer: DomSanitizer, public translate: TranslateService) {
super("hookshot_github", translate);
this.translate = translate;
}
public ngOnInit() {
super.ngOnInit();
this.loadingConnections = true;
this.tryLoadOrgs();
this.hookshot.getAuthUrls().then(urls => {
this.orgAddAuthUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urls.orgUrl);
});
}
private tryOrgAuth() {
this.hookshot.getAuthUrls().then(urls => {
this.orgAuthUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urls.orgUrl);
this.loadingConnections = false;
this.timerId = setTimeout(() => {
this.tryLoadOrgs();
}, 1000);
});
}
private tryLoadOrgs() {
this.hookshot.getInstalledLocations().then(r => {
this.authUrl = null;
if (r.length <= 0) {
this.tryOrgAuth();
return;
}
this.orgToRepoMap = {};
for (const orgDto of r) {
this.orgToRepoMap[orgDto.organization.name] = orgDto;
}
this.orgs = Object.keys(this.orgToRepoMap);
this.orgId = this.orgs[0];
this.orgAuthUrl = null;
this.loadRepos();
if (this.timerId) {
clearTimeout(this.timerId);
}
}).catch(e => {
if (e.status === 403 && e.error.dim_errcode === "T2B_NOT_LOGGED_IN") {
this.hookshot.getAuthUrls().then(urls => {
this.authUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urls.userUrl);
this.loadingConnections = false;
this.timerId = setTimeout(() => {
this.tryLoadOrgs();
}, 1000);
});
} else if (e.status === 400 && e.error.dim_errcode === "T2B_MISSING_AUTH") {
this.tryOrgAuth();
} else {
console.error(e);
this.translate.get('Error getting Github information').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
}
public loadRepos() {
this.isBusy = true;
const dto = this.orgToRepoMap[this.orgId];
this.repos = dto.repositories.map(r => r.name);
this.repoId = this.repos[0];
this.orgEditAuthUrl = dto.changeSelectionUrl ? this.sanitizer.bypassSecurityTrustResourceUrl(dto.changeSelectionUrl) : null;
if (this.isBridged) {
const conn = this.bridge.config.connections[0].config;
this.bridgedRepoSlug = `${conn.org}/${conn.repo}`;
}
this.isBusy = false;
this.loadingConnections = false;
}
public get isBridged(): boolean {
return this.bridge.config.connections.length > 0;
}
public async bridgeRoom(): Promise<any> {
this.isBusy = true;
try {
await this.scalar.inviteUser(this.roomId, this.bridge.config.botUserId);
} catch (e) {
if (!e.response || !e.response.error || !e.response.error._error ||
e.response.error._error.message.indexOf("already in the room") === -1) {
this.isBusy = false;
this.translate.get('Error inviting bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
return;
}
}
await this.scalar.setUserPowerLevel(this.roomId, this.bridge.config.botUserId, 50);
this.hookshot.bridgeRoom(this.roomId, this.orgId, this.repoId).then(conn => {
this.bridge.config.connections.push(conn);
this.loadRepos();
this.isBusy = false;
this.translate.get('Bridge requested').subscribe((res: string) => {
this.toaster.pop("success", res);
});
}).catch(error => {
this.isBusy = false;
console.error(error);
this.translate.get('Error requesting bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
public unbridgeRoom(): void {
this.isBusy = true;
this.hookshot.unbridgeRoom(this.roomId).then(() => {
this.bridge.config.connections = [];
this.isBusy = false;
this.translate.get('Bridge removed').subscribe((res: string) => {
this.toaster.pop("success", res);
});
}).catch(error => {
this.isBusy = false;
console.error(error);
this.translate.get('Error removing bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -0,0 +1,41 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-spinner *ngIf="loadingConnections"></my-spinner>
<div *ngIf="!loadingConnections">
<div *ngIf="isBridged">
<p>{{'This room is bridged to' | translate}} <a [href]="bridgedProjectUrl" rel="noopener" target="_blank">{{bridgedProjectUrlUnsafe}}</a></p>
<button type="button" class="element-btn element-btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
<div *ngIf="!isBridged && authUrl">
<p>
{{'In order to bridge to Jira, you\'ll need to authorize the bridge to access your organization(s). Please click the button below to do so.' | translate}}
</p>
<a [href]="authUrl" rel="noopener" target="_blank" class="btn btn-lg btn-link">
<img src="/assets/img/avatars/jira.png" width="35" /> {{'Sign in with Jira' | translate}}
</a>
</div>
<div *ngIf="!isBridged && !authUrl">
<my-field
label="{{'Instance' | translate}}"
[asSelect]="true"
[(value)]="instance"
[disabled]="isBusy"
(valueChange)="loadProjects()"
[selectOptions]="instanceOptions"
></my-field>
<my-field
label="{{'Project' | translate}}"
[asSelect]="true"
[(value)]="project"
[disabled]="isBusy"
[selectOptions]="projectOptions"
></my-field>
<button type="button" class="element-btn" [disabled]="isBusy" (click)="bridgeRoom()">
Bridge
</button>
</div>
</div>
</ng-template>
</my-bridge-config>

View File

@ -0,0 +1,4 @@
.actions-col {
width: 120px;
text-align: center;
}

View File

@ -0,0 +1,163 @@
import { Component, OnInit } from "@angular/core";
import { BridgeComponent } from "../bridge.component";
import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
import { HookshotJiraApiService } from "../../../shared/services/integrations/hookshot-jira-api.service";
import {
FE_HookshotJiraConnection,
FE_HookshotJiraInstance,
FE_HookshotJiraProject
} from "../../../shared/models/hookshot_jira";
interface HookshotConfig {
botUserId: string;
connections: FE_HookshotJiraConnection[];
loggedIn: boolean;
instances?: FE_HookshotJiraInstance[];
}
@Component({
templateUrl: "hookshot-jira.bridge.component.html",
styleUrls: ["hookshot-jira.bridge.component.scss"],
})
export class HookshotJiraBridgeConfigComponent extends BridgeComponent<HookshotConfig> implements OnInit {
public isBusy: boolean;
public authUrl: SafeUrl;
public loadingConnections = true;
public bridgedProjectUrl: SafeUrl;
public bridgedProjectUrlUnsafe: string;
public instances: FE_HookshotJiraInstance[] = [];
public instance: string;
public projects: FE_HookshotJiraProject[] = [];
public project: string;
private timerId: any;
constructor(private hookshot: HookshotJiraApiService, private scalar: ScalarClientApiService, private sanitizer: DomSanitizer, public translate: TranslateService) {
super("hookshot_jira", translate);
}
public get instanceOptions(): {key: string, value: string}[] {
return this.instances.map(i => ({key: i.name, value: `${i.name} (${i.url})`}));
}
public get projectOptions(): {key: string, value: string}[] {
return this.projects.map(p => ({key: p.key, value: `${p.key} (${p.name})`}));
}
public ngOnInit() {
super.ngOnInit();
this.loadingConnections = true;
this.tryLoadInstances();
}
private tryLoadInstances() {
this.hookshot.getInstances().then(r => {
this.authUrl = null;
this.instances = r;
this.instance = this.instances[0].name;
this.loadProjects();
if (this.timerId) {
clearTimeout(this.timerId);
}
}).catch(e => {
if (e.status === 403 && e.error.dim_errcode === "T2B_NOT_LOGGED_IN") {
this.hookshot.getAuthUrl().then(url => {
this.authUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
this.loadingConnections = false;
this.timerId = setTimeout(() => {
this.tryLoadInstances();
}, 1000);
});
} else {
console.error(e);
this.translate.get('Error getting Jira information').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
}
public loadProjects() {
this.isBusy = true;
this.hookshot.getProjects(this.instance).then(projects => {
this.projects = projects;
this.project = this.projects[0].key;
if (this.isBridged) {
this.bridgedProjectUrlUnsafe = this.bridge.config.connections[0].config.url;
this.bridgedProjectUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.bridgedProjectUrlUnsafe);
}
this.isBusy = false;
this.loadingConnections = false;
}).catch(e => {
console.error(e);
this.isBusy = false;
this.translate.get('Error getting Jira information').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
public get isBridged(): boolean {
return this.bridge.config.connections.length > 0;
}
public async bridgeRoom(): Promise<any> {
this.isBusy = true;
try {
await this.scalar.inviteUser(this.roomId, this.bridge.config.botUserId);
} catch (e) {
if (!e.response || !e.response.error || !e.response.error._error ||
e.response.error._error.message.indexOf("already in the room") === -1) {
this.isBusy = false;
this.translate.get('Error inviting bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
return;
}
}
await this.scalar.setUserPowerLevel(this.roomId, this.bridge.config.botUserId, 50);
this.hookshot.bridgeRoom(this.roomId, this.instance, this.project).then(conn => {
this.bridge.config.connections.push(conn);
this.loadProjects();
this.isBusy = false;
this.translate.get('Bridge requested').subscribe((res: string) => {
this.toaster.pop("success", res);
});
}).catch(error => {
this.isBusy = false;
console.error(error);
this.translate.get('Error requesting bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
public unbridgeRoom(): void {
this.isBusy = true;
this.hookshot.unbridgeRoom(this.roomId).then(() => {
this.bridge.config.connections = [];
this.isBusy = false;
this.translate.get('Bridge removed').subscribe((res: string) => {
this.toaster.pop("success", res);
});
}).catch(error => {
this.isBusy = false;
console.error(error);
this.translate.get('Error removing bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -0,0 +1,29 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<h3>{{'New webhook' | translate}}</h3>
<form (submit)="newHook()" novalidate name="addForm">
<my-field
label="{{'Webhook name' | translate}}"
[placeholder]="'My Webhook' | translate"
[(value)]="webhookName"
[disabled]="isBusy"
></my-field>
<button type="button" class="element-btn" [disabled]="isBusy" (click)="newHook()">
{{'Add webhook' | translate}}
</button>
</form>
<h3 style="margin-top: 36px">{{'Webhooks' | translate}}</h3>
<p *ngIf="newConfig.connections.length === 0">{{'No webhooks configured for this room.' | translate}}</p>
<details *ngFor="let hook of newConfig.connections trackById">
<summary>{{hook.config.name || ('No name' | translate)}}</summary>
URL: <a [href]="hook.config.url" target="_blank">{{ hook.config.url }}</a>
<div style="padding-bottom: 16px">
<button type="button" class="element-btn element-btn-danger" [disabled]="isBusy"
(click)="removeHook(hook)">
{{'Remove webhook' | translate}}
</button>
</div>
</details>
</ng-template>
</my-bridge-config>

View File

@ -0,0 +1,3 @@
.webhook-url {
word-break: break-word;
}

View File

@ -0,0 +1,75 @@
import { Component } from "@angular/core";
import { BridgeComponent } from "../bridge.component";
import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service";
import { TranslateService } from "@ngx-translate/core";
import { FE_HookshotWebhookConnection } from "../../../shared/models/hookshot_webhook";
import { HookshotWebhookApiService } from "../../../shared/services/integrations/hookshot-webhook-api.service";
interface HookshotConfig {
botUserId: string;
connections: FE_HookshotWebhookConnection[];
}
@Component({
templateUrl: "hookshot-webhook.bridge.component.html",
styleUrls: ["hookshot-webhook.bridge.component.scss"],
})
export class HookshotWebhookBridgeConfigComponent extends BridgeComponent<HookshotConfig> {
public webhookName: string;
public isBusy = false;
constructor(private webhooks: HookshotWebhookApiService, private scalar: ScalarClientApiService, public translate: TranslateService) {
super("hookshot_webhook", translate);
}
public async newHook() {
this.isBusy = true;
try {
await this.scalar.inviteUser(this.roomId, this.newConfig.botUserId);
} catch (e) {
if (!e.response || !e.response.error || !e.response.error._error ||
e.response.error._error.message.indexOf("already in the room") === -1) {
this.isBusy = false;
this.translate.get('Error inviting bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
return;
}
}
this.webhooks.createWebhook(this.roomId, this.webhookName).then(hook => {
this.newConfig.connections.push(hook);
this.isBusy = false;
this.webhookName = "";
this.translate.get('Webhook created').subscribe((res: string) => {
this.toaster.pop("success", res);
});
}).catch(err => {
console.error(err);
this.isBusy = false;
this.translate.get('Error creating webhook').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
public removeHook(hook: FE_HookshotWebhookConnection) {
this.isBusy = true;
this.webhooks.deleteWebhook(this.roomId, hook.id).then(() => {
const idx = this.newConfig.connections.indexOf(hook);
if (idx !== -1) this.newConfig.connections.splice(idx, 1);
this.isBusy = false;
this.translate.get('Webhook deleted').subscribe((res: string) => {
this.toaster.pop("success", res);
});
}).catch(err => {
console.error(err);
this.isBusy = false;
this.translate.get('Error deleting webhook').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,90 +1,54 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-ibox [isCollapsible]="true">
<h5 class="my-ibox-title">
{{'Add an IRC channel' | translate}}
</h5>
<div class="my-ibox-content">
<div class="alert alert-info">
{{'Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.' | translate}}
</div>
<h3>{{'Add IRC channel' | translate}}</h3>
<div class="alert alert-info">
{{'Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.' | translate}}
</div>
<div *ngIf="channelStep === 1">
<my-field
label="{{'Network' | translate}}"
[asSelect]="true"
[(value)]="networkId"
[disabled]="loadingOps"
[selectOptions]="getNetworks()"
></my-field>
<my-field
label="{{'Channel name (without leading #)' | translate}}"
placeholder="example"
[(value)]="channel"
[disabled]="loadingOps"
></my-field>
<button type="button" class="element-btn" [disabled]="loadingOps"
(click)="loadOps()">
{{'Next' | translate}}
</button>
</div>
<div *ngIf="channelStep === 1">
<label class="label-block">
{{'Network' | translate}}
<select class="form-control form-control-sm" [(ngModel)]="networkId" [disabled]="loadingOps">
<option *ngFor="let network of getNetworks()" [ngValue]="network.id">
{{ network.name }}
</option>
</select>
</label>
<label class="label-block">
{{'Channel Name' | translate}}
</label>
<div class="input-group input-group-sm">
<div class="input-group-addon">#</div>
<input title="channel" type="text" class="form-control form-control-sm" [(ngModel)]="channel"
[disabled]="loadingOps">
</div>
<div style="margin-top: 25px">
<button type="button" class="btn btn-sm btn-primary" [disabled]="loadingOps"
(click)="loadOps()">
{{'Next' | translate}}
</button>
</div>
</div>
<div *ngIf="channelStep === 2">
<p>{{'The person selected here will be asked to approve or deny the bridge request.' | translate}}</p>
<my-field
label="{{'Operator' | translate}}"
[asSelect]="true"
[(value)]="op"
[disabled]="requestingBridge"
[selectOptions]="ops"
></my-field>
<button type="button" class="element-btn" [disabled]="requestingBridge"
(click)="requestBridge()">
{{'Request bridge' | translate}}
</button>
</div>
<div *ngIf="channelStep === 2">
<label class="label-block">
{{'Operator' | translate}}
<span class="text-muted ">{{'The person selected here will be asked to approve or deny the bridge request.' | translate}}</span>
<select class="form-control form-control-sm" [(ngModel)]="op" [disabled]="requestingBridge">
<option *ngFor="let op of ops" [ngValue]="op">{{ op }}</option>
</select>
</label>
<div style="margin-top: 25px">
<button type="button" class="btn btn-sm btn-primary" [disabled]="requestingBridge"
(click)="requestBridge()">
{{'Request Bridge' | translate}}
</button>
</div>
</div>
<h3 style="margin-top: 36px">{{'Bridged channels' | translate}}</h3>
<p *ngIf="getChannels().length === 0">{{'No channels are bridged to this room.' | translate}}</p>
<details *ngFor="let channel of getChannels()">
<summary>{{channel.name + ' on ' + channel.networkName + (channel.pending ? ' (pending)' : '')}}</summary>
<div style="padding-bottom: 16px">
<button type="button" class="element-btn element-btn-danger" [disabled]="isUpdating || channel.pending"
(click)="removeChannel(channel)">
{{'Unbridge channel' | translate}}
</button>
</div>
</my-ibox>
<my-ibox [isCollapsible]="true">
<h5 class="my-ibox-title">
{{'IRC Networks' | translate}}
</h5>
<div class="my-ibox-content">
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>{{'Channel' | translate}}</th>
<th>{{'Network' | translate}}</th>
<th class="actions-col">{{'Actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngIf="getChannels().length === 0">
<td colspan="3">{{'No bridged channels' | translate}}</td>
</tr>
<tr *ngFor="let channel of getChannels()">
<td>
{{ channel.name }}
<span *ngIf="channel.pending" class="text-muted">(pending)</span>
</td>
<td>{{ channel.networkName }}</td>
<td class="actions-col">
<button type="button" class="btn btn-sm btn-outline-danger"
[disabled]="isUpdating || channel.pending"
(click)="removeChannel(channel)">
<i class="far fa-trash-alt"></i> {{'Remove' | translate}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</my-ibox>
</details>
</ng-template>
</my-bridge-config>

View File

@ -41,17 +41,17 @@ export class IrcBridgeConfigComponent extends BridgeComponent<IrcConfig> {
}
private resetForm() {
this.networkId = this.getNetworks()[0].id;
this.networkId = this.getNetworks()[0].key;
this.channel = "";
this.ops = [];
this.channelStep = 1;
}
public getNetworks(): { id: string, name: string }[] {
public getNetworks(): { key: string, value: string }[] {
const ids = Object.keys(this.bridge.config.availableNetworks);
if (!this.networkId) setTimeout(() => this.networkId = ids[0], 0);
return ids.map(i => {
return {id: i, name: this.bridge.config.availableNetworks[i].name};
return {key: i, value: this.bridge.config.availableNetworks[i].name};
});
}

View File

@ -1,57 +1,48 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-ibox [isCollapsible]="false">
<h5 class="my-ibox-title">
{{'Bridge to Slack' | translate}}
</h5>
<div class="my-ibox-content" *ngIf="loadingTeams">
<my-spinner></my-spinner>
<my-spinner *ngIf="loadingTeams"></my-spinner>
<div *ngIf="!loadingTeams">
<div *ngIf="isBridged && bridge.config.link.isWebhook">
<p>{{'This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn\'t support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.' | translate}}</p>
<button type="button" class="element-btn element-btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
<div class="my-ibox-content" *ngIf="!loadingTeams">
<div *ngIf="isBridged && bridge.config.link.isWebhook">
{{'This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn\'t support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.' | translate}}
<br/>
<button type="button" class="btn btn-sm btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
<div *ngIf="isBridged && !bridge.config.link.isWebhook">
This room is bridged to "{{ bridge.config.link.channelName }}" on Slack.
<button type="button" class="btn btn-sm btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
<div *ngIf="!isBridged && needsAuth">
<p>
{{'In order to bridge Slack channels, you\'ll need to authorize the bridge to access your teams and channels. Please click the button below to do so.' | translate}}
</p>
<a [href]="authUrl" rel="noopener" target="_blank">
<img src="/assets/img/slack_auth_button.png" class="slack-auth-button" alt="sign in with slack"/>
</a>
</div>
<div *ngIf="!isBridged && !needsAuth">
<label class="label-block">
{{'Team' | translate}}
<select class="form-control form-control-sm" [(ngModel)]="teamId"
(change)="loadChannels()" [disabled]="isBusy">
<option *ngFor="let team of teams" [ngValue]="team.id">
{{ team.name }}
</option>
</select>
</label>
<label class="label-block">
{{'Channel' | translate}}
<select class="form-control form-control-sm" [(ngModel)]="channelId" [disabled]="isBusy">
<option *ngFor="let channel of channels" [ngValue]="channel.id">
{{ channel.name }}
</option>
</select>
</label>
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="bridgeRoom()">
Bridge
</button>
</div>
<div *ngIf="isBridged && !bridge.config.link.isWebhook">
<p>{{'This room is bridged to ' | translate}}"{{ bridge.config.link.channelName }}"{{' on Slack.' | translate}}</p>
<button type="button" class="element-btn element-btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
</my-ibox>
<div *ngIf="!isBridged && needsAuth">
<p>
{{'In order to bridge Slack channels, you\'ll need to authorize the bridge to access your teams and channels. Please click the button below to do so.' | translate}}
</p>
<a [href]="authUrl" rel="noopener" target="_blank">
<img src="/assets/img/slack_auth_button.png" class="slack-auth-button" alt="sign in with slack"/>
</a>
</div>
<div *ngIf="!isBridged && !needsAuth">
<my-field
label="{{'Team' | translate}}"
[asSelect]="true"
[(value)]="teamId"
[disabled]="isBusy"
(valueChange)="loadChannels()"
[selectOptions]="teamOptions"
></my-field>
<my-field
label="{{'Channel' | translate}}"
[asSelect]="true"
[(value)]="channelId"
[disabled]="isBusy"
[selectOptions]="channelOptions"
></my-field>
<button type="button" class="element-btn" [disabled]="isBusy" (click)="bridgeRoom()">
{{'Bridge' | translate}}
</button>
</div>
</div>
</ng-template>
</my-bridge-config>

View File

@ -33,6 +33,14 @@ export class SlackBridgeConfigComponent extends BridgeComponent<SlackConfig> imp
this.translate = translate;
}
public get teamOptions(): {key: string, value: string}[] {
return this.teams.map(t => ({key: t.id, value: t.name}));
}
public get channelOptions(): {key: string, value: string}[] {
return this.channels.map(t => ({key: t.id, value: t.name}));
}
public ngOnInit() {
super.ngOnInit();

View File

@ -1,34 +1,29 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-ibox [isCollapsible]="false">
<h5 class="my-ibox-title">
{{'Bridge to Telegram' | translate}}
</h5>
<div class="my-ibox-content">
<div *ngIf="isBridged">
{{'This room is bridged to on Telegram' | translate}} "{{ chatName }}" (<code>{{ chatId }}</code>) {{'on Telegram' | translate}}.
<div *ngIf="canUnbridge">
<button type="button" class="btn btn-sm btn-danger" [disabled]="isUpdating"
(click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
<span *ngIf="!canUnbridge">
{{'You do not have the necessary permissions in this room to unbridge the channel.' | translate}}
</span>
</div>
<div *ngIf="!isBridged">
<label class="label-block">
Chat ID
<span class="text-muted">{{'After inviting' | translate}} <a href="https://t.me/{{ botUsername }}" target="_blank">@{{ botUsername }}</a> {{'to your Telegram chat, run the command' | translate}} <code>/id</code> {{'in the Telegram room to get the chat ID.' | translate}}</span>
<input title="chat ID" type="text" class="form-control form-control-sm col-md-3"
[(ngModel)]="chatId" [disabled]="isUpdating"/>
</label>
<button type="button" class="btn btn-sm btn-primary" [disabled]="isUpdating" (click)="bridgeRoom()">
Bridge
</button>
</div>
<div *ngIf="isBridged">
{{'This room is bridged to on Telegram' | translate}} "{{ chatName }}" (<code>{{ chatId }}</code>) {{'on Telegram' | translate}}.
<div *ngIf="canUnbridge">
<button type="button" class="element-btn element-btn-danger" [disabled]="isUpdating"
(click)="unbridgeRoom()">
{{'Unbridge' | translate}}
</button>
</div>
</my-ibox>
<span *ngIf="!canUnbridge">
{{'You do not have the necessary permissions in this room to unbridge the channel.' | translate}}
</span>
</div>
<div *ngIf="!isBridged">
<p class="text-muted">{{'After inviting' | translate}} <a href="https://t.me/{{ botUsername }}" target="_blank">@{{ botUsername }}</a> {{'to your Telegram chat, run the command' | translate}} <code>/id</code> {{'in the Telegram room to get the chat ID.' | translate}}</p>
<my-field
label="{{'Chat ID' | translate}}"
type="number"
[(value)]="chatId"
[disabled]="isUpdating"
></my-field>
<button type="button" class="element-btn" [disabled]="isUpdating" (click)="bridgeRoom()">
Bridge
</button>
</div>
</ng-template>
</my-bridge-config>

View File

@ -3,9 +3,7 @@ import { BridgeComponent } from "../bridge.component";
import { TelegramApiService } from "../../../shared/services/integrations/telegram-api.service";
import { FE_PortalInfo } from "../../../shared/models/telegram";
import { TelegramAskUnbridgeComponent } from "./ask-unbridge/ask-unbridge.component";
import {
TelegramCannotUnbridgeComponent
} from "./cannot-unbridge/cannot-unbridge.component";
import { TelegramCannotUnbridgeComponent } from "./cannot-unbridge/cannot-unbridge.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@ -60,19 +58,19 @@ export class TelegramBridgeConfigComponent extends BridgeComponent<TelegramConfi
return this.bridge.config.portalInfo ? this.bridge.config.portalInfo.chatName : null;
}
public get chatId(): number {
return this.bridge.config.portalInfo ? this.bridge.config.portalInfo.chatId : 0;
public get chatId(): string {
return `${this.bridge.config.portalInfo ? this.bridge.config.portalInfo.chatId : 0}`;
}
public set chatId(n: number) {
public set chatId(n: string) {
if (!this.bridge.config.portalInfo) this.bridge.config.portalInfo = {
chatId: n,
chatId: Number(n),
chatName: null,
canUnbridge: false,
bridged: false,
roomId: this.roomId,
};
else this.bridge.config.portalInfo.chatId = n;
else this.bridge.config.portalInfo.chatId = Number(n);
}
public bridgeRoom(): void {

View File

@ -1,56 +1,29 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-ibox [isCollapsible]="true">
<h5 class="my-ibox-title">
{{'Add a new webhook' | translate}}
</h5>
<div class="my-ibox-content">
<label class="label-block">
{{'Webhook Name' | translate}}
<input title="webhook name" type="text" class="form-control form-control-sm"
[(ngModel)]="webhookName" [disabled]="isBusy">
</label>
<div style="margin-top: 25px">
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="newHook()">
{{'Create' | translate}}
</button>
</div>
<h3>{{'New webhook' | translate}}</h3>
<form (submit)="newHook()" novalidate name="addForm">
<my-field
label="{{'Webhook name' | translate}}"
[placeholder]="'My Webhook' | translate"
[(value)]="webhookName"
[disabled]="isBusy"
></my-field>
<button type="button" class="element-btn" [disabled]="isBusy" (click)="newHook()">
{{'Add webhook' | translate}}
</button>
</form>
<h3 style="margin-top: 36px">{{'Webhooks' | translate}}</h3>
<p *ngIf="newConfig.webhooks.length === 0">{{'No webhooks configured for this room.' | translate}}</p>
<details *ngFor="let hook of newConfig.webhooks trackById">
<summary>{{hook.label || ('No name' | translate)}}</summary>
URL: <a [href]="hook.url" target="_blank">{{ hook.url }}</a>
<div style="padding-bottom: 16px">
<button type="button" class="element-btn element-btn-danger" [disabled]="isBusy"
(click)="removeHook(hook)">
{{'Remove webhook' | translate}}
</button>
</div>
</my-ibox>
<my-ibox [isCollapsible]="true">
<h5 class="my-ibox-title">
Webhooks
</h5>
<div class="my-ibox-content">
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>Name</th>
<th>{{'Type' | translate}}</th>
<th>URL</th>
<th class="actions-col">{{'Actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngIf="newConfig.webhooks.length === 0">
<td colspan="4">{{'No webhooks' | translate}}</td>
</tr>
<tr *ngFor="let hook of newConfig.webhooks">
<td *ngIf="hook.label">{{ hook.label }}</td>
<td *ngIf="!hook.label"><i>{{'No name' | translate}}</i></td>
<td>{{ hook.type }}</td>
<td class="webhook-url"><a [href]="hook.url" target="_blank">{{ hook.url }}</a></td>
<td class="actions-col">
<button type="button" class="btn btn-sm btn-outline-danger"
[disabled]="isBusy"
(click)="removeHook(hook)">
<i class="far fa-trash-alt"></i> {{'Delete' | translate}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</my-ibox>
</details>
</ng-template>
</my-bridge-config>

View File

@ -1,13 +0,0 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{ bot.displayName }}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>{{ bot.description }}</p>
<ui-switch [checked]="bot._inRoom" [disabled]="bot._isUpdating" (change)="toggle()"></ui-switch>
</div>
<div class="modal-footer">
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Close' | translate}}
</button>
</div>

View File

@ -1,11 +1,14 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'New meeting' | translate}}"
editTitle="{{'Meetings' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'BigBlueButton Meeting URL' | translate}}
<input type="text" class="form-control"
placeholder="https://bbb.example.com/abc-def-ghi"
[(ngModel)]="widget.dimension.newData.conferenceUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Meeting URL' | translate}}"
placeholder="https://bbb.example.com/abc-def-ghi"
[(value)]="widget.dimension.conferenceUrl"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,5 +1,5 @@
import { WidgetComponent, DISABLE_AUTOMATIC_WRAPPING } from "../widget.component";
import { WIDGET_BIGBLUEBUTTON, EditableWidget } from "../../../shared/models/widget";
import { DISABLE_AUTOMATIC_WRAPPING, WidgetComponent } from "../widget.component";
import { EditableWidget, WIDGET_BIGBLUEBUTTON } from "../../../shared/models/widget";
import { Component } from "@angular/core";
import { FE_BigBlueButtonWidget } from "../../../shared/models/integration";
import { SessionStorage } from "../../../shared/SessionStorage";

View File

@ -2,44 +2,33 @@
<my-spinner></my-spinner>
</div>
<div *ngIf="!widgetComponent.isLoading">
<my-ibox [isCollapsible]="true" [defaultCollapsed]="!!widgetComponent.defaultExpandedWidgetId">
<h5 class="my-ibox-title">
<i class="far fa-plus-square"></i> {{ widgetComponent.defaultName | translate }} {{'Add' | translate}}
</h5>
<div class="my-ibox-content">
<form (submit)="widgetComponent.addWidget()" novalidate name="addForm">
<ng-container
*ngTemplateOutlet="widgetParamsTemplate;context:{widget:widgetComponent.newWidget}"></ng-container>
<div style="margin-top: 25px">
<button type="submit" class="btn btn-sm btn-success" [disabled]="widgetComponent.isUpdating">
<i class="fa fa-plus"></i> {{'Add Widget' | translate}}
</button>
</div>
</form>
<h3>{{addTitle}}</h3>
<form (submit)="widgetComponent.addWidget()" novalidate name="addForm">
<ng-container *ngTemplateOutlet="widgetParamsTemplate;context:{widget:widgetComponent.newWidget}"></ng-container>
<div class="element-btn-row">
<button type="submit" class="element-btn" [disabled]="widgetComponent.isUpdating">
{{'Add widget' | translate}}
</button>
</div>
</my-ibox>
</form>
<my-ibox *ngFor="let widget of widgetComponent.widgets trackById" [isCollapsible]="true"
[defaultCollapsed]="widget.id !== widgetComponent.defaultExpandedWidgetId">
<h5 class="my-ibox-title">
<i class="fa fa-pencil-alt"></i> {{ widget.name || widget.url || widgetComponent.defaultName }}
<span *ngIf="widget.data.title">- {{ widget.data.title }}</span>
</h5>
<div class="my-ibox-content">
<div *ngIf="widgetComponent.widgets.length > 0" style="margin-top: 36px">
<h3>{{editTitle}}</h3>
<details *ngFor="let widget of widgetComponent.widgets trackById" [open]="widget.id === widgetComponent.defaultExpandedWidgetId">
<summary>{{widget.name || widget.url || widgetComponent.defaultName}}</summary>
<form (submit)="widgetComponent.saveWidget(widget)" novalidate name="editForm">
<ng-container *ngTemplateOutlet="widgetParamsTemplate;context:{widget:widget}"></ng-container>
<div style="margin-top: 25px">
<button type="submit" class="btn btn-sm btn-primary" [disabled]="widgetComponent.isUpdating">
<i class="far fa-save"></i>{{'Save Widget' | translate}}
<div class="element-btn-row">
<button type="submit" class="element-btn" [disabled]="widgetComponent.isUpdating">
{{'Save' | translate}}
</button>
<button type="button" class="btn btn-sm ml-1 btn-outline-danger" [disabled]="widgetComponent.isUpdating"
<button type="button" class="element-btn element-btn-danger-link" [disabled]="widgetComponent.isUpdating"
(click)="widgetComponent.removeWidget(widget)">
<i class="far fa-trash-alt"></i>{{'Remove Widget' | translate}}
{{'Remove widget' | translate}}
</button>
</div>
</form>
</div>
</my-ibox>
</details>
</div>
</div>

View File

@ -8,8 +8,9 @@ import { Component, ContentChild, Input, TemplateRef } from "@angular/core";
})
export class ConfigScreenWidgetComponent {
@Input() widgetComponent: WidgetComponent;
@ContentChild(TemplateRef, { static: false })
widgetParamsTemplate: TemplateRef<any>;
@Input() addTitle: string;
@Input() editTitle: string;
@ContentChild(TemplateRef, { static: false }) widgetParamsTemplate: TemplateRef<any>;
constructor() {}
}

View File

@ -1,18 +1,20 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'Embed new website' | translate}}"
editTitle="{{'Embedded websites' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Widget Name' | translate}}
<input type="text" class="form-control"
placeholder="{{ defaultName | translate}}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Widget URL' | translate}}
<input type="text" class="form-control"
placeholder="https://matrix.org"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Site name' | translate}}"
[placeholder]="defaultName | translate"
[(value)]="widget.dimension.newName"
[disabled]="isUpdating"
></my-field>
<my-field
label="{{'Site URL' | translate}}"
placeholder="https://matrix.org"
[(value)]="widget.dimension.newUrl"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,19 +1,21 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'Create new notepad' | translate}}"
editTitle="{{'Notepads' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Pad Name' | translate}}
<input type="text" class="form-control"
maxlength="39"
placeholder="{{ defaultName }}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Pad URL' | translate}}
<input type="text" class="form-control"
placeholder="https://scalar.vector.im/etherpad/p/MyCoolPadName"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Pad name' | translate}}"
[placeholder]="defaultName"
[(value)]="widget.dimension.newName"
[disabled]="isUpdating"
[maxlength]="39"
></my-field>
<my-field
label="{{'Pad URL' | translate}}"
placeholder="https://scalar.vector.im/etherpad/p/MyCoolPadName"
[(value)]="widget.dimension.newUrl"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -20,7 +20,6 @@ export class EtherpadWidgetConfigComponent extends WidgetComponent {
}
protected OnWidgetsDiscovered(widgets: EditableWidget[]): void {
console.log(widgets);
for (const widget of widgets) {
if (!widget.dimension.newUrl.startsWith("http://") && !widget.dimension.newUrl.startsWith("https://")) {
const parsedUrl = url.parse(widget.url, true);

View File

@ -1,11 +1,14 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'New calendar' | translate}}"
editTitle="{{'Calendars' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Shared Calendar ID' | translate}}
<input type="text" class="form-control"
placeholder="en.uk#holiday@group.v.calendar.google.com"
[(ngModel)]="widget.dimension.newData.shareId" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Shared calendar ID' | translate}}"
placeholder="en.uk#holiday@group.v.calendar.google.com"
[(value)]="widget.dimension.shareId"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,11 +1,14 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'New document' | translate}}"
editTitle="{{'Documents' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Document URL' | translate}}
<input type="text" class="form-control"
placeholder="https://docs.google.com/document/d/1V0olL42WJ84LNYn0kFBJaPmlRxQ4Trx97a5wfVMuJC8/edit"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Document URL' | translate}}"
placeholder="https://docs.google.com/document/d/1V0olL42WJ84LNYn0kFBJaPmlRxQ4Trx97a5wfVMuJC8/edit"
[(value)]="widget.dimension.newUrl"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,19 +1,21 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'New dashboard' | translate}}"
editTitle="{{'Dashboards' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Grafana URL' | translate}}
<span class="text-muted">{{'To get a URL, go to Grafana and click \'share\' on a graph.' | translate}}</span>
<input type="text" class="form-control"
placeholder="https://example.com/grafana/dashboard/db/example?orgId=1&panelId=1&fullscreen"
[(ngModel)]="widget.dimension.newData.url" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Widget Name' | translate}}
<input type="text" class="form-control"
placeholder="{{ defaultName }}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<p>{{'To get a URL, go to Grafana and click \'share\' on a graph.' | translate}}</p>
<my-field
label="{{'Dashboard name' | translate}}"
placeholder="{{ defaultName | translate }}"
[(value)]="widget.dimension.newName"
[disabled]="isUpdating"
></my-field>
<my-field
label="{{'Dashboard URL' | translate}}"
placeholder="https://example.com/grafana/dashboard/db/example?orgId=1&panelId=1&fullscreen"
[(value)]="widget.dimension.newData.url"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,18 +1,14 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'Start conference' | translate}}"
editTitle="{{'Ongoing conferences' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Conference URL' | translate}}
<input type="text" class="form-control"
placeholder="https://jitsi.riot.im/MyCoolConference"
[(ngModel)]="widget.dimension.newData.conferenceUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<label class="label-block">
Audio only
<span class="text-muted">Starts the conference without camera by default. Users can still manually turn on the camera when joining or joined.</span>
<ui-switch size="medium"
[(ngModel)]="widget.dimension.newData.isAudioOnly" name="widget-audio-{{widget.id}}"
[disabled]="isUpdating"></ui-switch>
</label>
<my-field
label="{{'Conference URL' | translate}}"
placeholder="https://jitsi.riot.im/MyCoolConference"
[(value)]="widget.dimension.newData.conferenceUrl"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,12 +1,15 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'Embed music' | translate}}"
editTitle="{{'Embedded music' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{' Spotify URI' | translate}}
<span class="text-muted">{{'Click \'share\' from your favourite playlist, artist, track, or album and paste the Spotify URI here.' | translate}}</span>
<input type="text" class="form-control"
placeholder="spotify:artist:7CajNmpbOovFoOoasH2HaY"
[(ngModel)]="widget.dimension.newData.uri" name="widget-uri-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<p>{{'Click \'share\' from your favourite playlist, artist, track, or album and paste the Spotify URI here.' | translate}}</p>
<my-field
label="{{'Spotify URI' | translate}}"
placeholder="spotify:artist:7CajNmpbOovFoOoasH2HaY"
[(value)]="widget.dimension.newData.uri"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,23 +1,22 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'New dashboard' | translate}}"
editTitle="{{'Dashboards' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Trading Pair' | translate}}
<select class="form-control form-control-sm" [(ngModel)]="widget.dimension.newData.pair"
[disabled]="isUpdating" name="widget-pair-{{widget.id}}">
<option *ngFor="let pair of pairs" [ngValue]="pair.value">
{{ pair.label }}
</option>
</select>
</label>
<label class="label-block">
{{'Interval' | translate}}
<select class="form-control form-control-sm" [(ngModel)]="widget.dimension.newData.interval"
[disabled]="isUpdating" name="widget-interval-{{widget.id}}">
<option *ngFor="let interval of intervals" [ngValue]="interval.value">
{{ interval.label }}
</option>
</select>
</label>
<my-field
label="{{'Trading pair' | translate}}"
[asSelect]="true"
[(value)]="widget.dimension.newData.pair"
[disabled]="isUpdating"
[selectOptions]="pairs"
></my-field>
<my-field
label="{{'Interval' | translate}}"
[asSelect]="true"
[(value)]="widget.dimension.newData.interval"
[disabled]="isUpdating"
[selectOptions]="intervals"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -10,68 +10,68 @@ import { TranslateService } from "@ngx-translate/core";
export class TradingViewWidgetConfigComponent extends WidgetComponent {
public readonly intervals = [
{value: '1', label: '1 Minute'},
{value: '3', label: '3 Minutes'},
{value: '5', label: '5 Minutes'},
{value: '15', label: '15 Minutes'},
{value: '30', label: '30 Minutes'},
{value: '60', label: '1 Hour'},
{value: '120', label: '2 Hours'},
{value: '180', label: '3 Hours'},
{value: '240', label: '4 Hours'},
{value: 'D', label: '1 Day'},
{value: 'W', label: '1 Week'},
{key: '1', value: '1 Minute'},
{key: '3', value: '3 Minutes'},
{key: '5', value: '5 Minutes'},
{key: '15', value: '15 Minutes'},
{key: '30', value: '30 Minutes'},
{key: '60', value: '1 Hour'},
{key: '120', value: '2 Hours'},
{key: '180', value: '3 Hours'},
{key: '240', value: '4 Hours'},
{key: 'D', value: '1 Day'},
{key: 'W', value: '1 Week'},
];
public readonly pairs = [
// USD
{value: 'COINBASE:BTCUSD', label: 'Bitcoin / US Dollar'},
{value: 'COINBASE:ETHUSD', label: 'Ethereum / US Dollar'},
{value: 'COINBASE:LTCUSD', label: 'Litecoin / US Dollar'},
{value: 'BITTREX:SNTUSD', label: 'Status Network Token / US Dollar'},
{value: 'BITTREX:ETCUSD', label: 'Ethereum Classic / US Dollar'},
{value: 'BITFINEX:BTGUSD', label: 'BTG / US Dollar'},
{value: 'BITTREX:DASHUSD', label: 'Dash / US Dollar'},
{value: 'BITFINEX:EOSUSD', label: 'EOS / US Dollar'},
{value: 'BITFINEX:IOTUSD', label: 'IOTA / US Dollar'},
{value: 'BITTREX:LSKUSD', label: 'Lisk / US Dollar'},
{value: 'BITTREX:OMGUSD', label: 'OmiseGo / US Dollar'},
{value: 'BITTREX:NEOUSD', label: 'NEO / US Dollar'},
{value: 'BITTREX:XRPUSD', label: 'Ripple / US Dollar'},
{value: 'BITFINEX:ZECUSD', label: 'Zcash / US Dollar'},
{value: 'BITFINEX:XMRUSD', label: 'Monero / US Dollar'},
{key: 'COINBASE:BTCUSD', value: 'Bitcoin / US Dollar'},
{key: 'COINBASE:ETHUSD', value: 'Ethereum / US Dollar'},
{key: 'COINBASE:LTCUSD', value: 'Litecoin / US Dollar'},
{key: 'BITTREX:SNTUSD', value: 'Status Network Token / US Dollar'},
{key: 'BITTREX:ETCUSD', value: 'Ethereum Classic / US Dollar'},
{key: 'BITFINEX:BTGUSD', value: 'BTG / US Dollar'},
{key: 'BITTREX:DASHUSD', value: 'Dash / US Dollar'},
{key: 'BITFINEX:EOSUSD', value: 'EOS / US Dollar'},
{key: 'BITFINEX:IOTUSD', value: 'IOTA / US Dollar'},
{key: 'BITTREX:LSKUSD', value: 'Lisk / US Dollar'},
{key: 'BITTREX:OMGUSD', value: 'OmiseGo / US Dollar'},
{key: 'BITTREX:NEOUSD', value: 'NEO / US Dollar'},
{key: 'BITTREX:XRPUSD', value: 'Ripple / US Dollar'},
{key: 'BITFINEX:ZECUSD', value: 'Zcash / US Dollar'},
{key: 'BITFINEX:XMRUSD', value: 'Monero / US Dollar'},
// Euro / GBP
{value: 'COINBASE:BTCEUR', label: 'Bitcoin / Euro'},
{value: 'COINBASE:ETHEUR', label: 'Ethereum / Euro'},
{value: 'COINBASE:LTCEUR', label: 'Litecoin / Euro'},
{value: 'COINBASE:BTCGBP', label: 'Bitcoin / GBP'},
{key: 'COINBASE:BTCEUR', value: 'Bitcoin / Euro'},
{key: 'COINBASE:ETHEUR', value: 'Ethereum / Euro'},
{key: 'COINBASE:LTCEUR', value: 'Litecoin / Euro'},
{key: 'COINBASE:BTCGBP', value: 'Bitcoin / GBP'},
// Bitcoin
{value: 'COINBASE:ETHBTC', label: 'Ethereum / Bitcoin'},
{value: 'COINBASE:LTCBTC', label: 'Litecoin / Bitcoin'},
{value: 'BITTREX:SNTBTC', label: 'Status Network Token / Bitcoin'},
{value: 'BITTREX:BCCBTC', label: 'Bitcoin Cash / Bitcoin'},
{value: 'BITTREX:ADABTC', label: 'Ada / Bitcoin'},
{value: 'BITTREX:ARKBTC', label: 'Ark / Bitcoin'},
{value: 'BITTREX:EMC2BTC', label: 'Einsteinium / Bitcoin'},
{value: 'BITFINEX:IOTBTC', label: 'IOTA / Bitcoin'},
{value: 'BITTREX:LSKBTC', label: 'Lisk / Bitcoin'},
{value: 'BITTREX:NEOBTC', label: 'Neo / Bitcoin'},
{value: 'BITTREX:OMGBTC', label: 'OmiseGO / Bitcoin'},
{value: 'BITTREX:POWRBTC', label: 'PowerLedger / Bitcoin'},
{value: 'BITTREX:STRATBTC', label: 'Stratis / Bitcoin'},
{value: 'BITTREX:TRIGBTC', label: 'TRIG Token / Bitcoin'},
{value: 'BITTREX:VTCBTC', label: 'Vertcoin / Bitcoin'},
{value: 'BITTREX:XLMBTC', label: 'Lumen / Bitcoin'},
{value: 'BITTREX:XRPBTC', label: 'Ripple / Bitcoin'},
{key: 'COINBASE:ETHBTC', value: 'Ethereum / Bitcoin'},
{key: 'COINBASE:LTCBTC', value: 'Litecoin / Bitcoin'},
{key: 'BITTREX:SNTBTC', value: 'Status Network Token / Bitcoin'},
{key: 'BITTREX:BCCBTC', value: 'Bitcoin Cash / Bitcoin'},
{key: 'BITTREX:ADABTC', value: 'Ada / Bitcoin'},
{key: 'BITTREX:ARKBTC', value: 'Ark / Bitcoin'},
{key: 'BITTREX:EMC2BTC', value: 'Einsteinium / Bitcoin'},
{key: 'BITFINEX:IOTBTC', value: 'IOTA / Bitcoin'},
{key: 'BITTREX:LSKBTC', value: 'Lisk / Bitcoin'},
{key: 'BITTREX:NEOBTC', value: 'Neo / Bitcoin'},
{key: 'BITTREX:OMGBTC', value: 'OmiseGO / Bitcoin'},
{key: 'BITTREX:POWRBTC', value: 'PowerLedger / Bitcoin'},
{key: 'BITTREX:STRATBTC', value: 'Stratis / Bitcoin'},
{key: 'BITTREX:TRIGBTC', value: 'TRIG Token / Bitcoin'},
{key: 'BITTREX:VTCBTC', value: 'Vertcoin / Bitcoin'},
{key: 'BITTREX:XLMBTC', value: 'Lumen / Bitcoin'},
{key: 'BITTREX:XRPBTC', value: 'Ripple / Bitcoin'},
// Misc
{value: 'BITTREX:BTCUSDT', label: 'Bitcoin / Tether USD'},
{value: 'BITTREX:ETHUSDT', label: 'Ethereum / Tether USD'},
{value: 'BITTREX:SNTETH', label: 'Status Network Token / Ethereum'},
{value: 'BITTREX:BCCUSDT', label: 'Bitcoin Cash / Tether USD'},
{value: 'BITTREX:NEOUSDT', label: 'Neo / Tether'},
{key: 'BITTREX:BTCUSDT', value: 'Bitcoin / Tether USD'},
{key: 'BITTREX:ETHUSDT', value: 'Ethereum / Tether USD'},
{key: 'BITTREX:SNTETH', value: 'Status Network Token / Ethereum'},
{key: 'BITTREX:BCCUSDT', value: 'Bitcoin Cash / Tether USD'},
{key: 'BITTREX:NEOUSDT', value: 'Neo / Tether'},
];
constructor(public translate: TranslateService) {
@ -80,7 +80,7 @@ export class TradingViewWidgetConfigComponent extends WidgetComponent {
protected OnNewWidgetPrepared(widget: EditableWidget): void {
widget.dimension.newData.interval = "D"; // 1 day
widget.dimension.newData.pair = this.pairs[0].value;
widget.dimension.newData.pair = this.pairs[0].key;
}
protected OnWidgetBeforeAdd(widget: EditableWidget): void {
@ -92,8 +92,8 @@ export class TradingViewWidgetConfigComponent extends WidgetComponent {
}
private setViewUrl(widget: EditableWidget) {
const pair = this.pairs.find(p => p.value === widget.dimension.newData.pair);
widget.dimension.newTitle = pair ? pair.label : null;
const pair = this.pairs.find(p => p.key === widget.dimension.newData.pair);
widget.dimension.newTitle = pair ? pair.value : null;
widget.dimension.newUrl = window.location.origin + "/widgets/tradingview?pair=$pair&interval=$interval";
}
}

View File

@ -1,11 +1,14 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'Embed livestream' | translate}}"
editTitle="{{'Livestreams' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Channel Name' | translate}}
<input type="text" class="form-control"
placeholder="TwitchUsername"
[(ngModel)]="widget.dimension.newData.channelName" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Channel name' | translate}}"
placeholder="TwitchUsername"
[(value)]="widget.dimension.newData.channelName"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -1,18 +1,20 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'New whiteboard' | translate}}"
editTitle="{{'Whiteboards' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Whiteboard Name' | translate}}
<input type="text" class="form-control"
placeholder="{{ defaultName }}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Whiteboard URL' | translate}}
<input type="text" class="form-control"
placeholder=""
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Name' | translate}}"
placeholder="{{defaultName}}"
[(value)]="widget.dimension.newName"
[disabled]="isUpdating"
></my-field>
<my-field
label="{{'URL' | translate}}"
placeholder=""
[(value)]="widget.dimension.newUrl"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -19,7 +19,6 @@ export class WhiteboardWidgetComponent extends WidgetComponent {
}
protected OnWidgetsDiscovered(widgets: EditableWidget[]): void {
console.log(widgets);
for (const widget of widgets) {
if (!widget.dimension.newUrl.startsWith("http://") && !widget.dimension.newUrl.startsWith("https://")) {
const parsedUrl = url.parse(widget.url, true);

View File

@ -1,11 +1,14 @@
<my-widget-config [widgetComponent]="this">
<my-widget-config
[widgetComponent]="this"
addTitle="{{'New video' | translate}}"
editTitle="{{'Videos' | translate}}"
>
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
{{'Video URL' | translate}}
<input type="text" class="form-control"
placeholder="https://www.youtube.com/watch?v=jr2mXSKq3B4"
[(ngModel)]="widget.dimension.newData.videoUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
<my-field
label="{{'Video URL' | translate}}"
placeholder="https://www.youtube.com/watch?v=jr2mXSKq3B4"
[(value)]="widget.dimension.newData.videoUrl"
[disabled]="isUpdating"
></my-field>
</ng-template>
</my-widget-config>

View File

@ -6,8 +6,6 @@
</div>
<div *ngIf="!isLoading && !isError">
<div class="stickerpacks" *ngIf="hasStickerPacks">{{'Looking for your sticker packs?' | translate}}<a routerLink='stickerpicker'> {{'Click here' | translate}}</a>.</div>
<!-- ------------------------ -->
<!-- EMPTY/ENCRYPTED STATES -->
<!-- ------------------------ -->
@ -29,11 +27,10 @@
<!-- CATEGORIES -->
<!-- ------------------------ -->
<div *ngFor="let category of getCategories()">
<my-ibox *ngIf="getIntegrationsIn(category).length > 0" boxTitle="{{category}}" [isCollapsible]="true">
<div class="my-ibox-content">
<my-integration-bag [integrations]="getIntegrationsIn(category)"
(integrationClicked)="modifyIntegration($event)"></my-integration-bag>
</div>
</my-ibox>
<h3 *ngIf="getIntegrationsIn(category).length > 0">{{category}}</h3>
<my-integration-bag [integrations]="getIntegrationsIn(category)"
(integrationClicked)="modifyIntegration($event)"></my-integration-bag>
</div>
<h3>{{'Sticker packs' | translate}}</h3>
{{'Manage your sticker packs' | translate}} <a routerLink="stickerpicker">{{'here' | translate}}</a>
</div>

Some files were not shown because too many files have changed in this diff Show More