Initial support for matrix-hookshot#github

Missing:
* Bridge repo
* Unbridge repo
* Ignore hooks?
* List orgs and repos (or other sensible format)
* GH Auth
This commit is contained in:
Travis Ralston 2021-11-25 16:24:36 -07:00
parent 22b245bbd1
commit eb7dfb4f64
31 changed files with 1731 additions and 939 deletions

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,7 @@ 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_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, CACHE_TELEGRAM_BRIDGE } 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 AdminTelegramService {
@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 telegramBridge = await HookshotGithubBridgeRecord.findByPk(bridgeId);
if (!telegramBridge) throw new ApiError(404, "Github Bridge not found");
return {
id: telegramBridge.id,
upstreamId: telegramBridge.upstreamId,
provisionUrl: telegramBridge.provisionUrl,
sharedSecret: telegramBridge.sharedSecret,
isEnabled: telegramBridge.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("AdminTelegramService", 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,101 @@
import { LogService } from "matrix-bot-sdk";
import * as request from "request";
import HookshotGithubBridgeRecord from "../db/models/HookshotGithubBridgeRecord";
import {
HookshotConnection,
HookshotConnectionsResponse,
HookshotGithubRoomConfig,
HookshotTypes
} from "./models/hookshot";
export class HookshotGithubBridge {
constructor(private requestingUserId: string) {
}
private 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 isBridgingEnabled(): Promise<boolean> {
const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}});
return !!bridges && bridges.length > 0;
}
public async getRoomConfigurations(inRoomId: string): Promise<HookshotGithubRoomConfig[]> {
const bridge = await this.getDefaultBridge();
try {
const connections = await this.doProvisionRequest<HookshotConnectionsResponse>(bridge, "GET", `/v1/${inRoomId}/connections`);
return connections.filter(c => c.type === HookshotTypes.Github);
} catch (e) {
if (e.errBody['error'] === "Could not determine if the user is in the room.") {
return [];
}
throw e;
}
}
public async bridgeRoom(roomId: string): Promise<HookshotGithubRoomConfig> {
const bridge = await this.getDefaultBridge();
const body = {};
return await this.doProvisionRequest<HookshotConnection>(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}`);
}
private async doProvisionRequest<T>(bridge: HookshotGithubBridgeRecord, 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("TelegramBridge", "Doing provision Github 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("GithubHookshotBridge", "Error calling" + url);
LogService.error("GithubHookshotBridge", err);
reject(err);
} else if (!res) {
LogService.error("GithubHookshotBridge", "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("GithubHookshotBridge", "Got status code " + res.statusCode + " when calling " + url);
LogService.error("GithubHookshotBridge", 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("GithubHookshotBridge", e);
reject(e);
}
});
});
}
}

View File

@ -0,0 +1,16 @@
export interface HookshotConnection {
type: string;
id: string;
service: string; // human-readable
details: any; // context-specific
}
export type HookshotConnectionsResponse = HookshotConnection[];
export interface HookshotGithubRoomConfig {
}
export enum HookshotTypes {
Github = "uk.half-shot.matrix-hookshot.github.repository",
}

View File

@ -1,5 +1,5 @@
import {
Bridge,
Bridge, HookshotGithubBridgeConfiguration,
SlackBridgeConfiguration,
TelegramBridgeConfiguration,
WebhookBridgeConfiguration
@ -10,6 +10,7 @@ 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";
export class BridgeStore {
@ -59,7 +60,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"];
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 +79,9 @@ 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 return true;
}
@ -94,6 +98,9 @@ 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 return false;
}
@ -131,6 +138,14 @@ 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 connections = await hookshot.getRoomConfigurations(inRoomId);
return <HookshotGithubBridgeConfiguration>{
botUserId: "@hookshot_bot:localhost", // TODO
connections: connections,
};
} else return {};
}

View File

@ -29,6 +29,7 @@ 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";
class _DimensionStore {
private sequelize: Sequelize;
@ -75,6 +76,7 @@ class _DimensionStore {
TermsTextRecord,
TermsSignedRecord,
TermsUpstreamRecord,
HookshotGithubBridgeRecord,
]);
}

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,30 @@
import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
import Upstream from "./Upstream";
@Table({
tableName: "dimension_hookshot_github_bridges",
underscored: false,
timestamps: false,
})
export default class HookshotGithubBridgeRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@AllowNull
@Column
@ForeignKey(() => Upstream)
upstreamId?: number;
@AllowNull
@Column
provisionUrl?: string;
@AllowNull
@Column
sharedSecret?: string;
@Column
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,6 +16,7 @@ declare global {
}
LogService.setLevel(LogLevel.DEBUG);
LogService.setLogger(new RichConsoleLogger());
LogService.info("index", "Starting dimension " + CURRENT_VERSION);
async function startup() {

View File

@ -4,6 +4,7 @@ import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge";
import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge";
import { WebhookConfiguration } from "../bridges/models/webhooks";
import { BridgedChannel } from "../bridges/SlackBridge";
import { HookshotConnection } from "../bridges/models/hookshot";
const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks"];
@ -45,3 +46,8 @@ export interface SlackBridgeConfiguration {
link: BridgedChannel;
botUserId: string;
}
export interface HookshotGithubBridgeConfiguration {
botUserId: string;
connections: HookshotConnection[];
}

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,86 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import {
AdminHookshotGithubBridgeManageSelfhostedComponent,
ManageSelfhostedHookshotGithubBridgeDialogContext
} from "./manage-selfhosted/manage-selfhosted.component";
import { FE_TelegramBridge } from "../../../shared/models/telegram";
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_TelegramBridge[] = [];
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:9999/_matrix/provision/v1"
[(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,64 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { AdminTelegramApiService } from "../../../../shared/services/admin/admin-telegram-api.service";
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

@ -119,6 +119,11 @@ 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";
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient) {
@ -227,7 +232,10 @@ export function HttpLoaderFactory(http: HttpClient) {
AdminTermsNewEditPublishDialogComponent,
TermsWidgetWrapperComponent,
WhiteboardWidgetComponent,
AdminWidgetWhiteboardConfigComponent
AdminWidgetWhiteboardConfigComponent,
AdminHookshotGithubBridgeComponent,
AdminHookshotGithubBridgeManageSelfhostedComponent,
HookshotGithubBridgeConfigComponent,
// Vendor
],
@ -257,6 +265,8 @@ export function HttpLoaderFactory(http: HttpClient) {
AdminSlackApiService,
ToasterService,
AdminTermsApiService,
AdminHookshotGithubApiService,
HookshotGithubApiService,
{provide: Window, useValue: window},
// Vendor

View File

@ -48,6 +48,8 @@ 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";
const routes: Routes = [
{path: "", component: HomeComponent},
@ -134,6 +136,11 @@ const routes: Routes = [
component: AdminSlackBridgeComponent,
data: {breadcrumb: "Slack Bridge", name: "Slack Bridge"},
},
{
path: "hookshot_github",
component: AdminHookshotGithubBridgeComponent,
data: {breadcrumb: "Github Bridge", name: "Github Bridge"},
},
],
},
{
@ -272,6 +279,11 @@ const routes: Routes = [
component: SlackBridgeConfigComponent,
data: {breadcrumb: "Slack Bridge Configuration", name: "Slack Bridge Configuration"},
},
{
path: "hookshot_github",
component: HookshotGithubBridgeConfigComponent,
data: {breadcrumb: "Github Bridge Configuration", name: "Github Bridge Configuration"},
},
],
},
{

View File

@ -0,0 +1,44 @@
<my-bridge-config [bridgeComponent]="this">
<ng-template #bridgeParamsTemplate>
<my-ibox [isCollapsible]="false">
<h5 class="my-ibox-title">
{{'Bridge to Github' | translate}}
</h5>
<div class="my-ibox-content" *ngIf="loadingConnections">
<my-spinner></my-spinner>
</div>
<div class="my-ibox-content" *ngIf="!loadingConnections">
<div *ngIf="!isBridged && needsAuth">
<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">
<img src="/assets/img/slack_auth_button.png" alt="sign in with slack"/>
</a>
</div>
<div *ngIf="!isBridged && !needsAuth">
<label class="label-block">
{{'Organization' | translate}}
<select class="form-control form-control-sm" [(ngModel)]="orgId"
(change)="loadRepos()" [disabled]="isBusy">
<option *ngFor="let org of orgs" [ngValue]="org">
{{ org }}
</option>
</select>
</label>
<label class="label-block">
{{'Repository' | translate}}
<select class="form-control form-control-sm" [(ngModel)]="repoId" [disabled]="isBusy">
<option *ngFor="let repo of repos" [ngValue]="repo">
{{ repo }}
</option>
</select>
</label>
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="bridgeRoom()">
Bridge
</button>
</div>
</div>
</my-ibox>
</ng-template>
</my-bridge-config>

View File

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

View File

@ -0,0 +1,101 @@
import { Component, OnInit } from "@angular/core";
import { BridgeComponent } from "../bridge.component";
import { FE_SlackChannel, FE_SlackLink, FE_SlackTeam } from "../../../shared/models/slack";
import { SlackApiService } from "../../../shared/services/integrations/slack-api.service";
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_HookshotGithubBridge, FE_HookshotGithubConnection } 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 needsAuth = false;
public authUrl: SafeUrl;
public loadingConnections = false;
public orgs: string[] = [];
public repos: string[] = []; // for org
public orgId: string;
public repoId: string;
constructor(private hookshot: HookshotGithubApiService, private scalar: ScalarClientApiService, public translate: TranslateService) {
super("hookshot_github", translate);
this.translate = translate;
}
public ngOnInit() {
super.ngOnInit();
this.prepare();
}
private prepare() {
}
public loadRepos() {
// TODO
}
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;
}
}
this.hookshot.bridgeRoom(this.roomId).then(conn => {
this.bridge.config.connections.push(conn);
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

@ -161,6 +161,10 @@
<img src="/assets/img/avatars/slack.png">
<span>Slack</span>
</div>
<div class="integration">
<img src="/assets/img/avatars/github.png">
<span>GitHub</span>
</div>
<div class="integration">
<img src="/assets/img/avatars/webhooks.png">
<span>Webhooks</span>

View File

@ -0,0 +1,11 @@
export interface FE_HookshotGithubBridge {
id: number;
upstreamId?: number;
provisionUrl?: string;
sharedSecret?: string;
isEnabled: boolean;
}
export interface FE_HookshotGithubConnection {
}

View File

@ -31,6 +31,7 @@ export class IntegrationsRegistry {
"telegram": {},
"webhooks": {},
"slack": {},
"hookshot_github": {},
},
"widget": {
"custom": {

View File

@ -0,0 +1,39 @@
import { Injectable } from "@angular/core";
import { AuthedApi } from "../authed-api";
import { FE_Upstream } from "../../models/admin-responses";
import { FE_TelegramBridge, FE_TelegramBridgeOptions } from "../../models/telegram";
import { HttpClient } from "@angular/common/http";
import { FE_HookshotGithubBridge } from "../../models/hookshot_github";
@Injectable()
export class AdminHookshotGithubApiService extends AuthedApi {
constructor(http: HttpClient) {
super(http);
}
public getBridges(): Promise<FE_HookshotGithubBridge[]> {
return this.authedGet<FE_HookshotGithubBridge[]>("/api/v1/dimension/admin/hookshot/github/all").toPromise();
}
public getBridge(bridgeId: number): Promise<FE_HookshotGithubBridge> {
return this.authedGet<FE_HookshotGithubBridge>("/api/v1/dimension/admin/hookshot/github/" + bridgeId).toPromise();
}
public newFromUpstream(upstream: FE_Upstream): Promise<FE_HookshotGithubBridge> {
return this.authedPost<FE_HookshotGithubBridge>("/api/v1/dimension/admin/hookshot/github/new/upstream", {upstreamId: upstream.id}).toPromise();
}
public newSelfhosted(provisionUrl: string, sharedSecret: string): Promise<FE_HookshotGithubBridge> {
return this.authedPost<FE_HookshotGithubBridge>("/api/v1/dimension/admin/hookshot/github/new/selfhosted", {
provisionUrl: provisionUrl,
sharedSecret: sharedSecret,
}).toPromise();
}
public updateSelfhosted(bridgeId: number, provisionUrl: string, sharedSecret: string): Promise<FE_HookshotGithubBridge> {
return this.authedPost<FE_TelegramBridge>("/api/v1/dimension/admin/hookshot/github/" + bridgeId, {
provisionUrl: provisionUrl,
sharedSecret: sharedSecret,
}).toPromise();
}
}

View File

@ -0,0 +1,21 @@
import { Injectable } from "@angular/core";
import { AuthedApi } from "../authed-api";
import { HttpClient } from "@angular/common/http";
import { FE_HookshotGithubConnection } from "../../models/hookshot_github";
@Injectable()
export class HookshotGithubApiService extends AuthedApi {
constructor(http: HttpClient) {
super(http);
}
public bridgeRoom(roomId: string): Promise<FE_HookshotGithubConnection> {
return this.authedPost<FE_HookshotGithubConnection>("/api/v1/dimension/hookshot/github/room/" + roomId + "/connect", {
// TODO
}).toPromise();
}
public unbridgeRoom(roomId: string): Promise<any> {
return this.authedDelete("/api/v1/dimension/hookshot/github/room/" + roomId + "/connections/all").toPromise();
}
}

View File

@ -11,38 +11,39 @@
"Description": "Description",
"Actions": "Actions",
"No bridges.": "No bridges.",
"matrix-appservice-gitter": "matrix-appservice-gitter",
"is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.": "is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.",
"matrix-hookshot": "matrix-hookshot",
"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.": "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.",
"No bridge configurations.": "No bridge configurations.",
"Add matrix.org's bridge": "Add matrix.org's bridge",
"Add self-hosted bridge": "Add self-hosted bridge",
"self-hosted Gitter bridge": "self-hosted Gitter bridge ",
"Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.",
"self-hosted Github bridge": "self-hosted Github bridge",
"Self-hosted Github bridges must have": "Self-hosted Github bridges must have",
"provisioning": "provisioning",
"enabled in the configuration.": "enabled in the configuration.",
"Provisioning URL": "Provisioning URL",
"The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.",
"The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.": "The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.",
"Shared Secret": "Shared Secret",
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
"Save": "Save",
"Cancel": "Cancel",
"Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge ",
"Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have",
"provisioning": "provisioning",
"enabled in the configuration.": "enabled in the configuration.",
"The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.",
"matrix-appservice-irc": "matrix-appservice-irc",
"is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.": "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.",
"Enabled Networks": "Enabled Networks",
"This bridge is offline or unavailable.": "This bridge is offline or unavailable.",
"Add matrix.org's bridge": "Add matrix.org's bridge",
"Network": "Network",
"Enabled": "Enabled",
"Close": "Close",
"self-hosted Slack bridge": "self-hosted Slack bridge ",
"Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.",
"The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.",
"matrix-appservice-slack": "matrix-appservice-slack",
"is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.": "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.",
"self-hosted Telegram bridge": "self-hosted Telegram bridge ",
"Self-hosted Telegram bridges must have": "Self-hosted Telegram bridges must have",
"The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.": "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.",
"Shared Secret": "Shared Secret",
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
"Promote Telegram Puppeting": "Promote Telegram Puppeting",
"If enabled, Dimension will recommend that users log in to their Telegram accounts.": "If enabled, Dimension will recommend that users log in to their Telegram accounts.",
"Promote Matrix Puppeting": "Promote Matrix Puppeting",
@ -56,7 +57,7 @@
"The provisioning secret defined in the configuration.": "The provisioning secret defined in the configuration.",
"matrix-appservice-webhooks": "matrix-appservice-webhooks",
"provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.": "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.",
"custom bot": "",
"custom bot": "custom bot",
"The user ID that Dimension will invite to rooms.": "The user ID that Dimension will invite to rooms.",
"A few words here will help people understand what the bot does.": "A few words here will help people understand what the bot does.",
"Display Name": "Display Name",
@ -89,8 +90,8 @@
"The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.": "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.",
"New self-hosted go-neb": "New self-hosted go-neb",
"go-neb appservice configuration": "go-neb appservice configuration",
"Copy and paste this configuration to": "",
"on your homeserver and register it as an application service.": "",
"Copy and paste this configuration to": "Copy and paste this configuration to",
"on your homeserver and register it as an application service.": "on your homeserver and register it as an application service.",
"Test Configuration": "Test Configuration",
"Giphy Configuration": "Giphy Configuration",
"Api Key": "Api Key",
@ -100,6 +101,7 @@
"Use downsized images": "Use downsized images",
"Google Configuration": "Google Configuration",
"The API key for your Google Application.": "The API key for your Google Application.",
"Search Engine ID": "Search Engine ID",
"The search engine ID": "The search engine ID",
"Guggy Configuration": "Guggy Configuration",
"The API key for": "The API key for",
@ -122,7 +124,7 @@
"License": "License",
"No sticker packs installed.": "No sticker packs installed.",
"Dimension": "Dimension",
"version": "",
"version": "version",
"The translated name of your policy": "The translated name of your policy",
"Policy text": "Policy text",
"This is where you put your policy's content.": "This is where you put your policy's content.",
@ -148,11 +150,10 @@
"Jitsi Script URL": "Jitsi Script URL",
"This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.": "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.",
"Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.": "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.",
"Bridge to Gitter": "Bridge to Gitter",
"This room is bridged to on Gitter": "This room is bridged to",
"on Gitter": "on Gitter",
"Unbridge": "Unbridge",
"Gitter Room": "Gitter Room",
"Bridge to Github": "Bridge to Github",
"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.": "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.",
"Organization": "Organization",
"Repository": "Repository",
"Add an IRC channel": "Add an IRC channel",
"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.": "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.",
"Channel Name": "Channel Name",
@ -166,6 +167,7 @@
"Remove": "Remove",
"Bridge to Slack": "Bridge to Slack",
"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.": "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.",
"Unbridge": "Unbridge",
"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.": "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.",
"Team": "Team",
"Telegram chat is already bridged": "Telegram chat is already bridged",
@ -191,8 +193,8 @@
"Added by": "Added by",
"Add": "Add",
".travis.yml configuration and template information": ".travis.yml configuration and template information",
"The following section needs to be added to your": "",
"file in your repositories:": "",
"The following section needs to be added to your": "The following section needs to be added to your",
"file in your repositories:": "file in your repositories:",
"The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.": "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.",
"The repository identifier": "The repository identifier",
"The repository name": "The repository name",
@ -210,7 +212,6 @@
"A URL to see the changes which triggered the build": "A URL to see the changes which triggered the build",
"A URL to see the build information": "A URL to see the build information",
"Repositories": "Repositories",
"Repository": "",
"Template": "Template",
"Sticker packs are not enabled on this Dimension instance.": "Sticker packs are not enabled on this Dimension instance.",
"Start a conversation with": "Start a conversation with",
@ -218,7 +219,7 @@
"Add stickerpack": "Add stickerpack",
"Created by": "Created by",
"under": "under",
"BigBlueButton Meeting URL": "",
"BigBlueButton Meeting URL": "BigBlueButton Meeting URL",
"Add Widget": "Add Widget",
"Save Widget": "Save Widget",
"Remove Widget": "Remove Widget",
@ -231,7 +232,7 @@
"Grafana URL": "Grafana URL",
"To get a URL, go to Grafana and click 'share' on a graph.": "To get a URL, go to Grafana and click 'share' on a graph.",
"Conference URL": "Conference URL",
" Spotify URI": "",
" Spotify URI": " Spotify URI",
"Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.": "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.",
"Trading Pair": "Trading Pair",
"Interval": "Interval",
@ -271,6 +272,10 @@
"Join": "Join",
"for news and updates. Don't forget to star the repository on": "for news and updates. Don't forget to star the repository on",
"Here's the configuration options you'll need to update in your Element": "Here's the configuration options you'll need to update in your Element",
"The location of": "The location of ",
"differs depending on whether the": " differs depending on whether the",
"or": " or ",
"version of Element is used.": " version of Element is used.",
"Configuring integrations": "Configuring integrations",
"If everything is set up correctly, you'll be able to access the admin area of Dimension by clicking the 3x3 grid in the top right of any room in Element. The gear icon": "If everything is set up correctly, you'll be able to access the admin area of Dimension by clicking the 3x3 grid in the top right of any room in Element. The gear icon",
"in the top right is where you can configure your bots, bridges, and widgets.": "in the top right is where you can configure your bots, bridges, and widgets.",
@ -296,7 +301,7 @@
"BigBlueButton Conference": "BigBlueButton Conference",
"Join Conference": "Join Conference",
"Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded",
"Start camera:": "",
"Start camera:": "Start camera:",
"You": "You",
"Integrations": "Integrations",
"Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.": "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.",
@ -310,14 +315,11 @@
"Add some stickers": "Add some stickers",
"Failed to load bridges": "Failed to load bridges",
"Error loading bridges": "Error loading bridges",
"matrix.org's Gitter bridge added": "matrix.org's Gitter bridge added",
"Error adding matrix.org's Gitter Bridge": "Error adding matrix.org's Gitter Bridge",
"Error creating matrix.org's Gitter Bridge": "Error creating matrix.org's Gitter Bridge",
"Failed to get an update Gitter bridge list": "Failed to get an update Gitter bridge list",
"Gitter bridge added": "Gitter bridge added",
"Failed to create Gitter bridge": "Failed to create Gitter bridge",
"Gitter bridge updated": "Gitter bridge updated",
"Failed to update Gitter bridge": "Failed to update Gitter bridge",
"Failed to get an updated Github bridge list": "Failed to get an updated Github bridge list",
"Github bridge added": "Github bridge added",
"Failed to create Github bridge": "Failed to create Github bridge",
"Github bridge updated": "Github bridge updated",
"Failed to update Github bridge": "Failed to update Github bridge",
"IRC Bridge added": "IRC Bridge added",
"Failed to create IRC bridge": "Failed to create IRC bridge",
"Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.",
@ -438,8 +440,6 @@
"Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.": "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.",
"This integration is offline or unavailable": "This integration is offline or unavailable",
"Could not communicate with Element": "Could not communicate with Element",
"The room must be": "The room must be",
"to use this integration": "to use this integration",
"You cannot modify widgets in this room": "You cannot modify widgets in this room",
"Error communicating with Element": "Error communicating with Element",
"Expected to not be able to send specific event types": "Expected to not be able to send specific event types",
@ -459,9 +459,5 @@
"Checking client version...": "Checking client version...",
"Your client is too old to use this widget, sorry": "Your client is too old to use this widget, sorry",
"Please accept the prompt to verify your identity": "Please accept the prompt to verify your identity",
"Error loading policy": "Error loading policy",
"The location of": "The location of ",
"differs depending on whether the": " differs depending on whether the",
"or": " or ",
"version of Element is used.": " version of Element is used."
"Error loading policy": "Error loading policy"
}

View File

@ -11,38 +11,39 @@
"Description": "Description",
"Actions": "Actions",
"No bridges.": "No bridges.",
"matrix-appservice-gitter": "matrix-appservice-gitter",
"is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.": "is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.",
"matrix-hookshot": "matrix-hookshot",
"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.": "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.",
"No bridge configurations.": "No bridge configurations.",
"Add matrix.org's bridge": "Add matrix.org's bridge",
"Add self-hosted bridge": "Add self-hosted bridge",
"self-hosted Gitter bridge": "self-hosted Gitter bridge",
"Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.",
"self-hosted Github bridge": "self-hosted Github bridge",
"Self-hosted Github bridges must have": "Self-hosted Github bridges must have",
"provisioning": "provisioning",
"enabled in the configuration.": "enabled in the configuration.",
"Provisioning URL": "Provisioning URL",
"The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.",
"The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.": "The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.",
"Shared Secret": "Shared Secret",
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
"Save": "Save",
"Cancel": "Cancel",
"Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge",
"Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have",
"provisioning": "provisioning",
"enabled in the configuration.": "enabled in the configuration.",
"The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.",
"matrix-appservice-irc": "matrix-appservice-irc",
"is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.": "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.",
"Enabled Networks": "Enabled Networks",
"This bridge is offline or unavailable.": "This bridge is offline or unavailable.",
"Add matrix.org's bridge": "Add matrix.org's bridge",
"Network": "Network",
"Enabled": "Enabled",
"Close": "Close",
"self-hosted Slack bridge": "self-hosted Slack bridge",
"Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.",
"The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.",
"matrix-appservice-slack": "matrix-appservice-slack",
"is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.": "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.",
"self-hosted Telegram bridge": "self-hosted Telegram bridge",
"Self-hosted Telegram bridges must have": "Self-hosted Telegram bridges must have",
"The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.": "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.",
"Shared Secret": "Shared Secret",
"The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.",
"Promote Telegram Puppeting": "Promote Telegram Puppeting",
"If enabled, Dimension will recommend that users log in to their Telegram accounts.": "If enabled, Dimension will recommend that users log in to their Telegram accounts.",
"Promote Matrix Puppeting": "Promote Matrix Puppeting",
@ -100,6 +101,7 @@
"Use downsized images": "Use downsized images",
"Google Configuration": "Google Configuration",
"The API key for your Google Application.": "The API key for your Google Application.",
"Search Engine ID": "Search Engine ID",
"The search engine ID": "The search engine ID",
"Guggy Configuration": "Guggy Configuration",
"The API key for": "The API key for",
@ -148,11 +150,10 @@
"Jitsi Script URL": "Jitsi Script URL",
"This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.": "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.",
"Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.": "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.",
"Bridge to Gitter": "Bridge to Gitter",
"This room is bridged to on Gitter": "This room is bridged to on Gitter",
"on Gitter": "on Gitter",
"Unbridge": "Unbridge",
"Gitter Room": "Gitter Room",
"Bridge to Github": "Bridge to Github",
"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.": "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.",
"Organization": "Organization",
"Repository": "Repository",
"Add an IRC channel": "Add an IRC channel",
"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.": "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.",
"Channel Name": "Channel Name",
@ -166,6 +167,7 @@
"Remove": "Remove",
"Bridge to Slack": "Bridge to Slack",
"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.": "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.",
"Unbridge": "Unbridge",
"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.": "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.",
"Team": "Team",
"Telegram chat is already bridged": "Telegram chat is already bridged",
@ -210,7 +212,6 @@
"A URL to see the changes which triggered the build": "A URL to see the changes which triggered the build",
"A URL to see the build information": "A URL to see the build information",
"Repositories": "Repositories",
"Repository": "Repository",
"Template": "Template",
"Sticker packs are not enabled on this Dimension instance.": "Sticker packs are not enabled on this Dimension instance.",
"Start a conversation with": "Start a conversation with",
@ -271,6 +272,10 @@
"Join": "Join",
"for news and updates. Don't forget to star the repository on": "for news and updates. Don't forget to star the repository on",
"Here's the configuration options you'll need to update in your Element": "Here's the configuration options you'll need to update in your Element",
"The location of": "The location of",
"differs depending on whether the": "differs depending on whether the",
"or": "or",
"version of Element is used.": "version of Element is used.",
"Configuring integrations": "Configuring integrations",
"If everything is set up correctly, you'll be able to access the admin area of Dimension by clicking the 3x3 grid in the top right of any room in Element. The gear icon": "If everything is set up correctly, you'll be able to access the admin area of Dimension by clicking the 3x3 grid in the top right of any room in Element. The gear icon",
"in the top right is where you can configure your bots, bridges, and widgets.": "in the top right is where you can configure your bots, bridges, and widgets.",
@ -310,14 +315,11 @@
"Add some stickers": "Add some stickers",
"Failed to load bridges": "Failed to load bridges",
"Error loading bridges": "Error loading bridges",
"matrix.org's Gitter bridge added": "matrix.org's Gitter bridge added",
"Error adding matrix.org's Gitter Bridge": "Error adding matrix.org's Gitter Bridge",
"Error creating matrix.org's Gitter Bridge": "Error creating matrix.org's Gitter Bridge",
"Failed to get an update Gitter bridge list": "Failed to get an update Gitter bridge list",
"Gitter bridge added": "Gitter bridge added",
"Failed to create Gitter bridge": "Failed to create Gitter bridge",
"Gitter bridge updated": "Gitter bridge updated",
"Failed to update Gitter bridge": "Failed to update Gitter bridge",
"Failed to get an updated Github bridge list": "Failed to get an updated Github bridge list",
"Github bridge added": "Github bridge added",
"Failed to create Github bridge": "Failed to create Github bridge",
"Github bridge updated": "Github bridge updated",
"Failed to update Github bridge": "Failed to update Github bridge",
"IRC Bridge added": "IRC Bridge added",
"Failed to create IRC bridge": "Failed to create IRC bridge",
"Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.",
@ -438,8 +440,6 @@
"Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.": "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.",
"This integration is offline or unavailable": "This integration is offline or unavailable",
"Could not communicate with Element": "Could not communicate with Element",
"The room must be": "The room must be",
"to use this integration": "to use this integration",
"You cannot modify widgets in this room": "You cannot modify widgets in this room",
"Error communicating with Element": "Error communicating with Element",
"Expected to not be able to send specific event types": "Expected to not be able to send specific event types",
@ -459,9 +459,5 @@
"Checking client version...": "Checking client version...",
"Your client is too old to use this widget, sorry": "Your client is too old to use this widget, sorry",
"Please accept the prompt to verify your identity": "Please accept the prompt to verify your identity",
"Error loading policy": "Error loading policy",
"The location of": "The location of",
"differs depending on whether the": "differs depending on whether the",
"or": "or",
"version of Element is used.": "version of Element is used."
"Error loading policy": "Error loading policy"
}