mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
Add skeleton for admin room widget
This commit is contained in:
parent
4abf02da2a
commit
5c278a2b53
@ -39,6 +39,17 @@ logging:
|
||||
# Run openssl genpkey -out passkey.pem -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096
|
||||
passFile: "passkey.pem"
|
||||
|
||||
webhook:
|
||||
port: 9000
|
||||
|
||||
widgets:
|
||||
# The port to listen on for the widget API
|
||||
port: 5000
|
||||
# Public url that the widget API is reachable on
|
||||
publicUrl: "https://example.com/bridgewidgets/"
|
||||
# Add the widget to admin rooms
|
||||
addToAdminRooms: true
|
||||
|
||||
bot:
|
||||
displayname: "GitHub Bot"
|
||||
avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d
|
27
package.json
27
package.json
@ -9,39 +9,46 @@
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"build:web": "snowpack build",
|
||||
"build:app": "tsc --project tsconfig.json",
|
||||
"dev:web": "snowpack dev",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"build": "yarn run build:web && yarn run build:app",
|
||||
"prepare": "yarn build",
|
||||
"start": "node lib/App/BridgeApp.js",
|
||||
"start:app": "node lib/App/BridgeApp.js",
|
||||
"start:webhooks": "node lib/App/GithubWebhookApp.js",
|
||||
"start:matrixsender": "node lib/App/MatrixSenderApp.js",
|
||||
"start": "node --require source-map-support/register lib/App/BridgeApp.js",
|
||||
"start:app": "node --require source-map-support/register lib/App/BridgeApp.js",
|
||||
"start:webhooks": "node --require source-map-support/register lib/App/GithubWebhookApp.js",
|
||||
"start:matrixsender": "node --require source-map-support/register lib/App/MatrixSenderApp.js",
|
||||
"test": "mocha -r ts-node/register tests/*.ts",
|
||||
"lint": "eslint -c .eslintrc.js src/**/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "^2.10.2",
|
||||
"@octokit/auth-token": "^2.4.3",
|
||||
"@octokit/rest": "^18.0.9",
|
||||
"@prefresh/snowpack": "^2.2.0",
|
||||
"@octokit/auth-app": "2.10.2",
|
||||
"@octokit/auth-token": "^2.4.4",
|
||||
"@octokit/rest": "18.0.9",
|
||||
"axios": "^0.21.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"fontsource-open-sans": "^3.1.5",
|
||||
"ioredis": "^4.19.2",
|
||||
"markdown-it": "^12.0.2",
|
||||
"matrix-bot-sdk": "^0.5.8",
|
||||
"matrix-widget-api": "^0.1.0-beta.10",
|
||||
"micromatch": "^4.0.2",
|
||||
"mime": "^2.4.6",
|
||||
"mini.css": "^3.0.1",
|
||||
"mocha": "^8.2.1",
|
||||
"node-emoji": "^1.10.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"source-map-support": "^0.5.19",
|
||||
"string-argv": "v0.3.1",
|
||||
"uuid": "^8.3.1",
|
||||
"winston": "^3.3.3",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prefresh/snowpack": "^2.2.0",
|
||||
"@snowpack/plugin-typescript": "^1.1.1",
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/ioredis": "^4.17.8",
|
||||
"@types/markdown-it": "^10.0.3",
|
||||
@ -53,11 +60,13 @@
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.8.1",
|
||||
"@typescript-eslint/parser": "^4.8.1",
|
||||
"autoprefixer": "^10.1.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-plugin-mocha": "^8.0.0",
|
||||
"preact": "^10.5.7",
|
||||
"snowpack": "^2.18.3",
|
||||
"tailwind": "^4.0.0",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.1.2"
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
/** @type {import("snowpack").SnowpackUserConfig } */
|
||||
module.exports = {
|
||||
mount: {
|
||||
"web/": '/',
|
||||
"web/": '/'
|
||||
},
|
||||
plugins: [
|
||||
['@snowpack/plugin-typescript', '--project tsconfig.web.json'],
|
||||
'@prefresh/snowpack',
|
||||
'@prefresh/snowpack',
|
||||
['@snowpack/plugin-typescript', '--project tsconfig.web.json'],
|
||||
],
|
||||
install: [
|
||||
/* ... */
|
||||
],
|
||||
installOptions: {
|
||||
installTypes: true,
|
||||
polyfillNode: true,
|
||||
},
|
||||
devOptions: {
|
||||
/* ... */
|
||||
|
@ -15,6 +15,7 @@ import { GetUserResponse } from "./Gitlab/Types";
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { MatrixMessageContent } from "./MatrixEvent";
|
||||
import { ProjectsListForUserResponseData, ProjectsListForRepoResponseData } from "@octokit/types";
|
||||
import { BridgeRoomState } from "./Widgets/BridgeWidgetInterface";
|
||||
|
||||
|
||||
const md = new markdown();
|
||||
@ -44,6 +45,7 @@ export interface AdminAccountData {
|
||||
}
|
||||
export class AdminRoom extends EventEmitter {
|
||||
public static helpMessage: MatrixMessageContent;
|
||||
private widgetAccessToken = `abcdef`;
|
||||
static botCommands: BotCommands;
|
||||
|
||||
private pendingOAuthState: string|null = null;
|
||||
@ -54,6 +56,8 @@ export class AdminRoom extends EventEmitter {
|
||||
private tokenStore: UserTokenStore,
|
||||
private config: BridgeConfig) {
|
||||
super();
|
||||
// TODO: Move this
|
||||
this.backfillAccessToken();
|
||||
}
|
||||
|
||||
public get userId() {
|
||||
@ -64,6 +68,10 @@ export class AdminRoom extends EventEmitter {
|
||||
return this.pendingOAuthState;
|
||||
}
|
||||
|
||||
public verifyWidgetAccessToken(token: string) {
|
||||
return this.widgetAccessToken === token;
|
||||
}
|
||||
|
||||
public notificationsEnabled(type: "github"|"gitlab", instanceName?: string) {
|
||||
if (type === "github") {
|
||||
return this.data.github?.notifications?.enabled;
|
||||
@ -140,6 +148,7 @@ export class AdminRoom extends EventEmitter {
|
||||
const octokit = GithubInstance.createUserOctokit(accessToken);
|
||||
me = await octokit.users.getAuthenticated();
|
||||
} catch (ex) {
|
||||
log.error("Failed to auth with GitHub", ex);
|
||||
await this.sendNotice("Could not authenticate with GitHub. Is your token correct?");
|
||||
return;
|
||||
}
|
||||
@ -444,6 +453,77 @@ export class AdminRoom extends EventEmitter {
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
public async getBridgeState(): Promise<BridgeRoomState> {
|
||||
const gitHubEnabled = !!this.config.github;
|
||||
const github: {enabled: boolean; tokenStored: boolean; identity: null|{name: string|null; avatarUrl: string|null}} = {
|
||||
enabled: false,
|
||||
tokenStored: false,
|
||||
identity: null,
|
||||
};
|
||||
if (gitHubEnabled) {
|
||||
const octokit = await this.tokenStore.getOctokitForUser(this.userId);
|
||||
try {
|
||||
const identity = await octokit?.users.getAuthenticated();
|
||||
github.enabled = true;
|
||||
github.tokenStored = !!octokit;
|
||||
github.identity = {
|
||||
name: identity?.data.login || null,
|
||||
avatarUrl: identity?.data.avatar_url || null,
|
||||
};
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to get user identity: ${ex}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: "Admin Room",
|
||||
github,
|
||||
}
|
||||
}
|
||||
|
||||
public async setupWidget() {
|
||||
try {
|
||||
const res = await this.botIntent.underlyingClient.getRoomStateEvent(this.roomId, "im.vector.modular.widgets", "bridge_control");
|
||||
if (res) {
|
||||
// No-op
|
||||
// Validate?
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
// Didn't exist, create it.
|
||||
}
|
||||
const accessToken = uuid();
|
||||
return this.botIntent.underlyingClient.sendStateEvent(
|
||||
this.roomId,
|
||||
"im.vector.modular.widgets",
|
||||
"bridge_control",
|
||||
{
|
||||
"creatorUserId": this.botIntent.userId,
|
||||
"data": {
|
||||
"title": "Bridge Control"
|
||||
},
|
||||
"id": "bridge_control",
|
||||
"name": "Bridge Control",
|
||||
"type": "m.custom",
|
||||
"url": `${this.config.widgets?.publicUrl}/#/?roomId=$matrix_room_id&widgetId=$matrix_widget_id&accessToken=${accessToken}`,
|
||||
accessToken,
|
||||
"waitForIframeLoad": true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillAccessToken() {
|
||||
try {
|
||||
const res = await this.botIntent.underlyingClient.getRoomStateEvent(this.roomId, "im.vector.modular.widgets", "bridge_control");
|
||||
if (res) {
|
||||
log.debug(`Stored access token for widgets for ${this.roomId}`);
|
||||
this.widgetAccessToken = res.accessToken;
|
||||
}
|
||||
} catch (ex) {
|
||||
log.info(`No widget access token for ${this.roomId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -41,6 +41,12 @@ interface BridgeConfigGitLab {
|
||||
instances: {[name: string]: GitLabInstance};
|
||||
}
|
||||
|
||||
interface BridgeWidgetConfig {
|
||||
port: number;
|
||||
addToAdminRooms: boolean;
|
||||
publicUrl: string;
|
||||
}
|
||||
|
||||
export interface BridgeConfig {
|
||||
github?: BridgeConfigGitHub;
|
||||
gitlab?: BridgeConfigGitLab;
|
||||
@ -69,6 +75,7 @@ export interface BridgeConfig {
|
||||
displayname?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
widgets?: BridgeWidgetConfig;
|
||||
}
|
||||
|
||||
export async function parseRegistrationFile(filename: string) {
|
||||
|
@ -26,7 +26,7 @@ import { IGitLabWebhookIssueStateEvent, IGitLabWebhookMREvent, IGitLabWebhookNot
|
||||
import { GitLabIssueConnection } from "./Connections/GitlabIssue";
|
||||
import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
// import { IGitLabWebhookMREvent } from "./Gitlab/WebhookTypes";
|
||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||
|
||||
const log = new LogWrapper("GithubBridge");
|
||||
|
||||
@ -39,6 +39,7 @@ export class GithubBridge {
|
||||
private queue!: MessageQueue;
|
||||
private tokenStore!: UserTokenStore;
|
||||
private messageClient!: MessageSenderClient;
|
||||
private widgetApi!: BridgeWidgetApi;
|
||||
|
||||
private connections: IConnection[] = [];
|
||||
|
||||
@ -169,6 +170,8 @@ export class GithubBridge {
|
||||
storage,
|
||||
});
|
||||
|
||||
this.widgetApi = new BridgeWidgetApi(this.adminRooms);
|
||||
|
||||
this.commentProcessor = new CommentProcessor(this.as, this.config.bridge.mediaUrl);
|
||||
|
||||
this.tokenStore = new UserTokenStore(this.config.passFile || "./passkey.pem", this.as.botIntent);
|
||||
@ -392,7 +395,13 @@ export class GithubBridge {
|
||||
|
||||
for (const roomId of joinedRooms) {
|
||||
log.debug("Fetching state for " + roomId);
|
||||
const connections = await this.createConnectionsForRoomId(roomId);
|
||||
let connections: IConnection[];
|
||||
try {
|
||||
connections = await this.createConnectionsForRoomId(roomId);
|
||||
} catch (ex) {
|
||||
log.error(`Unable to create connection for ${roomId}`, ex);
|
||||
continue;
|
||||
}
|
||||
this.connections.push(...connections);
|
||||
if (connections.length === 0) {
|
||||
// TODO: Refactor this to be a connection
|
||||
@ -400,7 +409,7 @@ export class GithubBridge {
|
||||
const accountData = await this.as.botIntent.underlyingClient.getRoomAccountData(
|
||||
BRIDGE_ROOM_TYPE, roomId,
|
||||
);
|
||||
const adminRoom = this.setupAdminRoom(roomId, accountData);
|
||||
const adminRoom = await this.setupAdminRoom(roomId, accountData);
|
||||
// Call this on startup to set the state
|
||||
await this.onAdminRoomSettingsChanged(adminRoom, accountData, { admin_user: accountData.admin_user });
|
||||
} catch (ex) {
|
||||
@ -410,7 +419,9 @@ export class GithubBridge {
|
||||
log.info(`Room ${roomId} is connected to: ${connections.join(',')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.widgets) {
|
||||
await this.widgetApi.start(this.config.widgets.port);
|
||||
}
|
||||
await this.as.begin();
|
||||
log.info("Started bridge");
|
||||
}
|
||||
@ -427,7 +438,7 @@ export class GithubBridge {
|
||||
}
|
||||
await retry(() => this.as.botIntent.joinRoom(roomId), 5);
|
||||
if (event.content.is_direct) {
|
||||
const room = this.setupAdminRoom(roomId, {admin_user: event.sender});
|
||||
const room = await this.setupAdminRoom(roomId, {admin_user: event.sender});
|
||||
await this.as.botIntent.underlyingClient.setRoomAccountData(
|
||||
BRIDGE_ROOM_TYPE, roomId, room.data,
|
||||
);
|
||||
@ -652,7 +663,7 @@ export class GithubBridge {
|
||||
|
||||
}
|
||||
|
||||
private setupAdminRoom(roomId: string, accountData: AdminAccountData) {
|
||||
private async setupAdminRoom(roomId: string, accountData: AdminAccountData) {
|
||||
const adminRoom = new AdminRoom(
|
||||
roomId, accountData, this.as.botIntent, this.tokenStore, this.config,
|
||||
);
|
||||
@ -680,6 +691,9 @@ export class GithubBridge {
|
||||
return this.as.botClient.inviteUser(adminRoom.userId, newConnection.roomId);
|
||||
});
|
||||
this.adminRooms.set(roomId, adminRoom);
|
||||
if (this.config.widgets?.addToAdminRooms && this.config.widgets.publicUrl) {
|
||||
await adminRoom.setupWidget();
|
||||
}
|
||||
log.info(`Setup ${roomId} as an admin room for ${adminRoom.userId}`);
|
||||
return adminRoom;
|
||||
}
|
||||
|
72
src/Widgets/BridgeWidgetApi.ts
Normal file
72
src/Widgets/BridgeWidgetApi.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Application, default as express, Request, Response } from "express";
|
||||
import cors from "cors";
|
||||
import { AdminRoom } from "../AdminRoom";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
|
||||
const log = new LogWrapper("GithubBridge");
|
||||
|
||||
export class BridgeWidgetApi {
|
||||
private app: Application;
|
||||
constructor(private adminRooms: Map<string, AdminRoom>) {
|
||||
this.app = express();
|
||||
this.app.use((req, _res, next) => {
|
||||
log.info(`${req.method} ${req.path} ${req.ip || ''} ${req.headers["user-agent"] || ''}`);
|
||||
next();
|
||||
});
|
||||
this.app.use('/', express.static('public'));
|
||||
this.app.use(cors());
|
||||
this.app.get('/widgetapi/:roomId/verify', this.getVerifyToken.bind(this));
|
||||
this.app.get('/widgetapi/:roomId', this.getRoomState.bind(this));
|
||||
this.app.get('/health', this.getHealth.bind(this));
|
||||
}
|
||||
|
||||
public start(port = 5000) {
|
||||
log.info(`Widget API listening on port ${port}`)
|
||||
this.app.listen(port);
|
||||
}
|
||||
|
||||
private async getRoomFromRequest(req: Request): Promise<AdminRoom|{error: string, statusCode: number}> {
|
||||
const { roomId } = req.params;
|
||||
const token = req.headers.authorization?.substr('Bearer '.length);
|
||||
if (!token) {
|
||||
return {
|
||||
error: 'Access token not given',
|
||||
statusCode: 400,
|
||||
};
|
||||
}
|
||||
// Replace with actual auth
|
||||
const room = this.adminRooms.get(roomId);
|
||||
if (!room || !room.verifyWidgetAccessToken(token)) {
|
||||
return {error: 'Unauthorized access to room', statusCode: 401}
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
|
||||
private async getVerifyToken(req: Request, res: Response) {
|
||||
const roomOrError = await this.getRoomFromRequest(req);
|
||||
if (roomOrError instanceof AdminRoom) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
return res.status(roomOrError.statusCode).send({error: roomOrError.error});
|
||||
}
|
||||
|
||||
private async getRoomState(req: Request, res: Response) {
|
||||
const roomOrError = await this.getRoomFromRequest(req);
|
||||
if (!(roomOrError instanceof AdminRoom)) {
|
||||
return res.status(roomOrError.statusCode).send({error: roomOrError.error});
|
||||
}
|
||||
try {
|
||||
return res.send(await roomOrError.getBridgeState());
|
||||
} catch (ex) {
|
||||
log.error(`Failed to get room state:`, ex);
|
||||
return res.status(500).send({error: "An error occured when getting room state"});
|
||||
}
|
||||
}
|
||||
|
||||
private getHealth(req: Request, res: Response) {
|
||||
res.status(200).send({ok: true});
|
||||
}
|
||||
}
|
11
src/Widgets/BridgeWidgetInterface.ts
Normal file
11
src/Widgets/BridgeWidgetInterface.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface BridgeRoomState {
|
||||
title: string;
|
||||
github: {
|
||||
enabled: boolean;
|
||||
tokenStored: boolean;
|
||||
identity: {
|
||||
name: string|null;
|
||||
avatarUrl: string|null;
|
||||
}|null;
|
||||
}
|
||||
}
|
@ -25,6 +25,9 @@
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"tests/**/*",
|
||||
"web/**/*"
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"include": ["src", "types"],
|
||||
"include": ["web"],
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "h",
|
||||
@ -17,7 +17,10 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importsNotUsedAsValues": "error"
|
||||
"importsNotUsedAsValues": "error",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
126
web/App.tsx
126
web/App.tsx
@ -1,37 +1,93 @@
|
||||
import { h } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
function App() {
|
||||
// Create the count state.
|
||||
const [count, setCount] = useState(0);
|
||||
// Create the counter (+1 every second).
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setCount(count + 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [count, setCount]);
|
||||
// Return the App component.
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>
|
||||
Edit <code>src/App.jsx</code> and save to reload.
|
||||
</p>
|
||||
<p>
|
||||
Page has been open for <code>{count}</code> seconds.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://preactjs.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn Preact
|
||||
</a>
|
||||
</p>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
import { h, Component } from 'preact';
|
||||
import WA from 'matrix-widget-api';
|
||||
import BridgeAPI from './BridgeAPI';
|
||||
import { BridgeRoomState } from '../src/Widgets/BridgeWidgetInterface';
|
||||
import ErrorPane from './components/ErrorPane';
|
||||
import AdminSettings from './components/AdminSettings';
|
||||
interface IState {
|
||||
error: string|null,
|
||||
busy: boolean,
|
||||
roomId?: string,
|
||||
roomState?: BridgeRoomState;
|
||||
}
|
||||
|
||||
export default App;
|
||||
function parseFragment() {
|
||||
const fragmentString = (window.location.hash || "?");
|
||||
return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf('?'), 0)));
|
||||
}
|
||||
|
||||
function assertParam(fragment, name) {
|
||||
const val = fragment.get(name);
|
||||
if (!val) throw new Error(`${name} is not present in URL - cannot load widget`);
|
||||
return val;
|
||||
}
|
||||
|
||||
export default class App extends Component<void, IState> {
|
||||
private widgetApi: WA.WidgetApi;
|
||||
private bridgeApi: BridgeAPI;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
error: null,
|
||||
busy: true,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
// Start widgeting
|
||||
const qs = parseFragment();
|
||||
const widgetId = assertParam(qs, 'widgetId');
|
||||
const roomId = assertParam(qs, 'roomId');
|
||||
const accessToken = assertParam(qs, 'accessToken');
|
||||
this.bridgeApi = new BridgeAPI("http://localhost:5000", roomId, accessToken);
|
||||
await this.bridgeApi.verify();
|
||||
this.widgetApi = new WA.WidgetApi(widgetId);
|
||||
this.widgetApi.on("ready", () => {
|
||||
console.log("Widget ready:", this);
|
||||
});
|
||||
this.widgetApi.on(`action:${WA.WidgetApiToWidgetAction.NotifyCapabilities}`, (ev) => {
|
||||
console.log(ev.detail.data.approved);
|
||||
console.log(`${WA.WidgetApiToWidgetAction.NotifyCapabilities}`, ev);
|
||||
})
|
||||
this.widgetApi.on(`action:${WA.WidgetApiToWidgetAction.SendEvent}`, (ev) => {
|
||||
console.log(`${WA.WidgetApiToWidgetAction.SendEvent}`, ev);
|
||||
})
|
||||
// Start the widget as soon as possible too, otherwise the client might time us out.
|
||||
this.widgetApi.start();
|
||||
const roomState = await this.bridgeApi.state();
|
||||
console.log('Got state', roomState);
|
||||
this.setState({
|
||||
roomState,
|
||||
roomId,
|
||||
busy: false,
|
||||
});
|
||||
} catch (ex) {
|
||||
console.error(`Bridge verifiation failed:`, ex);
|
||||
this.setState({
|
||||
error: ex.message,
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Return the App component.
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = <ErrorPane>{this.state.error}</ErrorPane>;
|
||||
} else if (this.state.roomState) {
|
||||
content = <AdminSettings roomState={this.state.roomState}></AdminSettings>;
|
||||
} else if (this.state.busy) {
|
||||
content = <div class="spinner"></div>;
|
||||
} else {
|
||||
content = <b>Invalid state</b>;
|
||||
}
|
||||
return (
|
||||
<div className="App">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
41
web/BridgeAPI.ts
Normal file
41
web/BridgeAPI.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { BridgeRoomState } from '../src/Widgets/BridgeWidgetInterface';
|
||||
|
||||
export class BridgeAPIError extends Error {
|
||||
constructor(msg: string, private body: Record<string, unknown>) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export default class BridgeAPI {
|
||||
|
||||
constructor(private baseUrl: string, private roomId: string, private accessToken: string) {
|
||||
}
|
||||
|
||||
async request(method: string, endpoint: string, body?: unknown) {
|
||||
const req = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
cache: 'no-cache',
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
},
|
||||
});
|
||||
if (req.status === 204) {
|
||||
return;
|
||||
}
|
||||
if (req.status === 200) {
|
||||
return req.json();
|
||||
}
|
||||
const resultBody = await req.json();
|
||||
throw new BridgeAPIError(resultBody?.error || 'Request failed', resultBody);
|
||||
}
|
||||
|
||||
async verify() {
|
||||
return this.request('GET', `/widgetapi/${encodeURIComponent(this.roomId)}/verify`);
|
||||
}
|
||||
|
||||
async state(): Promise<BridgeRoomState> {
|
||||
return this.request('GET', `/widgetapi/${encodeURIComponent(this.roomId)}`);
|
||||
}
|
||||
}
|
0
web/IBridgeEvents.ts
Normal file
0
web/IBridgeEvents.ts
Normal file
7
web/components/AdminSettings.css
Normal file
7
web/components/AdminSettings.css
Normal file
@ -0,0 +1,7 @@
|
||||
.adminsettings > .card {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.adminsettings > h1 {
|
||||
text-align: center;
|
||||
}
|
43
web/components/AdminSettings.tsx
Normal file
43
web/components/AdminSettings.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { BridgeRoomState } from "../../src/Widgets/BridgeWidgetInterface";
|
||||
import "./AdminSettings.css";
|
||||
import LoginCard from './LoginCard';
|
||||
|
||||
interface IProps{
|
||||
roomState: BridgeRoomState;
|
||||
}
|
||||
|
||||
export default class AdminSettings extends Component<IProps> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
renderGitHub() {
|
||||
const githubConfig = this.props.roomState.github;
|
||||
if (!githubConfig.enabled) {
|
||||
return <strong>
|
||||
GitHub support is not enabled in the bridge
|
||||
</strong>
|
||||
}
|
||||
if (!githubConfig.tokenStored) {
|
||||
return <strong>
|
||||
You have not logged into GitHub
|
||||
</strong>
|
||||
}
|
||||
if (!githubConfig.identity) {
|
||||
return <strong>
|
||||
Your token does not appear to work
|
||||
</strong>;
|
||||
}
|
||||
return <LoginCard name={githubConfig.identity.name} avatarUrl={githubConfig.identity.avatarUrl}/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div class="adminsettings">
|
||||
<h1>{this.props.roomState.title}</h1>
|
||||
<h2>GitHub</h2>
|
||||
{this.renderGitHub()}
|
||||
<h2>GitLab</h2>
|
||||
</div>;
|
||||
}
|
||||
}
|
3
web/components/ErrorPane.css
Normal file
3
web/components/ErrorPane.css
Normal file
@ -0,0 +1,3 @@
|
||||
.error-pane {
|
||||
max-width: 480px;
|
||||
}
|
11
web/components/ErrorPane.tsx
Normal file
11
web/components/ErrorPane.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { h, FunctionComponent } from "preact";
|
||||
import "./ErrorPane.css";
|
||||
|
||||
const ErrorPane: FunctionComponent<unknown> = ({ children }) => {
|
||||
return <div class="card error error-pane">
|
||||
<h3>Error occured during widget load</h3>
|
||||
<p>{children}</p>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default ErrorPane;
|
7
web/components/LoginCard.css
Normal file
7
web/components/LoginCard.css
Normal file
@ -0,0 +1,7 @@
|
||||
.login-card img {
|
||||
height: 15vw;
|
||||
}
|
||||
|
||||
.login-card span {
|
||||
font-size: 1.5rem;
|
||||
}
|
17
web/components/LoginCard.tsx
Normal file
17
web/components/LoginCard.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { h, FunctionComponent } from 'preact';
|
||||
import "./LoginCard.css";
|
||||
|
||||
const LoginCard: FunctionComponent<{name: string; avatarUrl: string;}> = ({ name, avatarUrl }) => {
|
||||
return <div class="container login-card">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<img src={avatarUrl} title="GitHub avatar"/>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
Logged in as <span>{name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default LoginCard;
|
@ -2,24 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Web site created using create-snowpack-app" />
|
||||
<title>Snowpack App</title>
|
||||
<title>GitHub Bridge Widget</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<main id="root"></main>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script type="module" src="/index.js"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { h, render } from 'preact';
|
||||
import 'preact/devtools';
|
||||
import App from './App';
|
||||
import "./styling.css";
|
||||
import "fontsource-open-sans/files/open-sans-latin-400-normal.woff2";
|
||||
|
||||
const root = document.getElementById('root')
|
||||
const root = document.getElementsByTagName('main')[0];
|
||||
|
||||
if (root) {
|
||||
render(<App />, root);
|
||||
|
7
web/styling.css
Normal file
7
web/styling.css
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
@import "fontsource-open-sans/400-normal.css";
|
||||
@import "mini.css";
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans';
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user