mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-08-31 06:02:09 +00:00
Self-service requests to bridge IRC channels
This commit is contained in:
parent
1e437a2f8b
commit
f33f7e5716
@ -46,14 +46,7 @@ export class DimensionIntegrationsService {
|
|||||||
* @returns {Promise<Bridge[]>} Resolves to the bridge list
|
* @returns {Promise<Bridge[]>} Resolves to the bridge list
|
||||||
*/
|
*/
|
||||||
public static async getBridges(enabledOnly: boolean, forUserId: string, inRoomId?: string): Promise<Bridge[]> {
|
public static async getBridges(enabledOnly: boolean, forUserId: string, inRoomId?: string): Promise<Bridge[]> {
|
||||||
const cacheKey = inRoomId ? "bridges_" + inRoomId : "bridges";
|
return BridgeStore.listAll(forUserId, enabledOnly ? true : null, inRoomId);
|
||||||
|
|
||||||
const cached = Cache.for(CACHE_INTEGRATIONS).get(cacheKey);
|
|
||||||
if (cached) return cached;
|
|
||||||
|
|
||||||
const bridges = await BridgeStore.listAll(forUserId, enabledOnly ? true : null, inRoomId);
|
|
||||||
Cache.for(CACHE_INTEGRATIONS).put(cacheKey, bridges);
|
|
||||||
return bridges;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
49
src/api/dimension/DimensionIrcService.ts
Normal file
49
src/api/dimension/DimensionIrcService.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
|
import { LogService } from "matrix-js-snippets";
|
||||||
|
import { ScalarService } from "../scalar/ScalarService";
|
||||||
|
import { IrcBridge } from "../../bridges/IrcBridge";
|
||||||
|
import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
|
||||||
|
import { ApiError } from "../ApiError";
|
||||||
|
|
||||||
|
interface RequestLinkRequest {
|
||||||
|
op: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API for interacting with the IRC bridge
|
||||||
|
*/
|
||||||
|
@Path("/api/v1/dimension/irc")
|
||||||
|
export class DimensionIrcService {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path(":networkId/channel/:channel/ops")
|
||||||
|
public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
|
||||||
|
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
|
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||||
|
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||||
|
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||||
|
|
||||||
|
const client = new IrcBridge(userId);
|
||||||
|
const operators = await client.getOperators(bridge, parsed.bridgeNetworkId, "#" + channelNoHash);
|
||||||
|
|
||||||
|
LogService.info("DimensionIrcService", userId + " listed the operators for #" + channelNoHash + " on " + networkId);
|
||||||
|
return operators;
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path(":networkId/channel/:channel/link/:roomId")
|
||||||
|
public async requestLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise<any> {
|
||||||
|
const userId = await ScalarService.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
|
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||||
|
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||||
|
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||||
|
|
||||||
|
const client = new IrcBridge(userId);
|
||||||
|
await client.requestLink(bridge, parsed.bridgeNetworkId, "#" + channelNoHash, request.op, roomId);
|
||||||
|
|
||||||
|
LogService.info("DimensionIrcService", userId + " requested #" + channelNoHash + " on " + networkId + " to be linked to " + roomId);
|
||||||
|
return {}; // 200 OK
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,9 @@ import Upstream from "../db/models/Upstream";
|
|||||||
import UserScalarToken from "../db/models/UserScalarToken";
|
import UserScalarToken from "../db/models/UserScalarToken";
|
||||||
import { LogService } from "matrix-js-snippets";
|
import { LogService } from "matrix-js-snippets";
|
||||||
import * as request from "request";
|
import * as request from "request";
|
||||||
import { QueryNetworksResponse } from "./models/provision_responses";
|
import { ListLinksResponseItem, ListOpsResponse, QueryNetworksResponse } from "./models/irc";
|
||||||
import { ModularIrcQueryNetworksResponse } from "../models/ModularResponses";
|
|
||||||
import IrcBridgeNetwork from "../db/models/IrcBridgeNetwork";
|
import IrcBridgeNetwork from "../db/models/IrcBridgeNetwork";
|
||||||
|
import { ModularIrcResponse } from "../models/ModularResponses";
|
||||||
|
|
||||||
interface CachedNetwork {
|
interface CachedNetwork {
|
||||||
ircBridgeId: number;
|
ircBridgeId: number;
|
||||||
@ -27,6 +27,12 @@ export interface AvailableNetworks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LinkedChannels {
|
||||||
|
[networkId: string]: {
|
||||||
|
channelName: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
export class IrcBridge {
|
export class IrcBridge {
|
||||||
private static getNetworkId(network: CachedNetwork): string {
|
private static getNetworkId(network: CachedNetwork): string {
|
||||||
return network.ircBridgeId + "-" + network.bridgeNetworkId;
|
return network.ircBridgeId + "-" + network.bridgeNetworkId;
|
||||||
@ -47,9 +53,10 @@ export class IrcBridge {
|
|||||||
return allNetworks.length > 0;
|
return allNetworks.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNetworks(bridge?: IrcBridgeRecord): Promise<AvailableNetworks> {
|
public async getNetworks(bridge?: IrcBridgeRecord, enabledOnly?: boolean): Promise<AvailableNetworks> {
|
||||||
let networks = await this.getAllNetworks();
|
let networks = await this.getAllNetworks();
|
||||||
if (bridge) networks = networks.filter(n => n.ircBridgeId === bridge.id);
|
if (bridge) networks = networks.filter(n => n.ircBridgeId === bridge.id);
|
||||||
|
if (enabledOnly) networks = networks.filter(n => n.isEnabled);
|
||||||
|
|
||||||
const available: AvailableNetworks = {};
|
const available: AvailableNetworks = {};
|
||||||
networks.forEach(n => available[IrcBridge.getNetworkId(n)] = {
|
networks.forEach(n => available[IrcBridge.getNetworkId(n)] = {
|
||||||
@ -61,12 +68,63 @@ export class IrcBridge {
|
|||||||
return available;
|
return available;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRoomConfiguration(requestingUserId: string, inRoomId: string): Promise<IrcBridgeConfiguration> {
|
public async getRoomConfiguration(inRoomId: string): Promise<IrcBridgeConfiguration> {
|
||||||
return <any>{requestingUserId, inRoomId};
|
const availableNetworks = await this.getNetworks(null, true);
|
||||||
|
const bridges = await IrcBridgeRecord.findAll({where: {isEnabled: true}});
|
||||||
|
const linkedChannels: LinkedChannels = {};
|
||||||
|
|
||||||
|
for (const bridge of bridges) {
|
||||||
|
const links = await this.fetchLinks(bridge, inRoomId);
|
||||||
|
for (const key of Object.keys(links)) {
|
||||||
|
linkedChannels[key] = links[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {availableNetworks: availableNetworks, links: linkedChannels};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRoomConfiguration(requestingUserId: string, inRoomId: string, newConfig: IrcBridgeConfiguration): Promise<any> {
|
public async getOperators(bridge: IrcBridgeRecord, networkId: string, channel: string): Promise<string[]> {
|
||||||
return <any>{requestingUserId, inRoomId, newConfig};
|
const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId);
|
||||||
|
if (!network) throw new Error("Network not found");
|
||||||
|
|
||||||
|
const requestBody = {remote_room_server: network.domain, remote_room_channel: channel};
|
||||||
|
|
||||||
|
let responses: ListOpsResponse[] = [];
|
||||||
|
if (bridge.upstreamId) {
|
||||||
|
const result = await this.doUpstreamRequest<ModularIrcResponse<ListOpsResponse>>(bridge, "POST", "/bridges/irc/_matrix/provision/querylink", null, requestBody);
|
||||||
|
if (result && result.replies) responses = result.replies.map(r => r.response);
|
||||||
|
} else {
|
||||||
|
const result = await this.doProvisionRequest<ListOpsResponse>(bridge, "POST", "/_matrix/provision/querylink", null, requestBody);
|
||||||
|
if (result) responses = [result];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ops: string[] = [];
|
||||||
|
for (const response of responses) {
|
||||||
|
if (!response || !response.operators) continue;
|
||||||
|
response.operators.forEach(i => ops.push(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async requestLink(bridge: IrcBridgeRecord, networkId: string, channel: string, op: string, inRoomId: string): Promise<any> {
|
||||||
|
const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId);
|
||||||
|
if (!network) throw new Error("Network not found");
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
remote_room_server: network.domain,
|
||||||
|
remote_room_channel: channel,
|
||||||
|
matrix_room_id: inRoomId,
|
||||||
|
op_nick: op,
|
||||||
|
user_id: this.requestingUserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bridge.upstreamId) {
|
||||||
|
delete requestBody["user_id"];
|
||||||
|
await this.doUpstreamRequest(bridge, "POST", "/bridges/irc/_matrix/provision/link", null, requestBody);
|
||||||
|
} else {
|
||||||
|
await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/link", null, requestBody);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAllNetworks(): Promise<CachedNetwork[]> {
|
private async getAllNetworks(): Promise<CachedNetwork[]> {
|
||||||
@ -86,10 +144,50 @@ export class IrcBridge {
|
|||||||
return networks;
|
return networks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fetchLinks(bridge: IrcBridgeRecord, inRoomId: string): Promise<LinkedChannels> {
|
||||||
|
const availableNetworks = await this.getNetworks(bridge, true);
|
||||||
|
const networksByDomain: { [domain: string]: { id: string, name: string, bridgeUserId: string } } = {};
|
||||||
|
for (const key of Object.keys(availableNetworks)) {
|
||||||
|
const network = availableNetworks[key];
|
||||||
|
networksByDomain[network.domain] = {
|
||||||
|
id: key,
|
||||||
|
name: network.name,
|
||||||
|
bridgeUserId: network.bridgeUserId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let responses: ListLinksResponseItem[] = [];
|
||||||
|
if (bridge.upstreamId) {
|
||||||
|
const result = await this.doUpstreamRequest<ModularIrcResponse<ListLinksResponseItem[]>>(bridge, "GET", "/bridges/irc/_matrix/provision/listlinks/" + inRoomId);
|
||||||
|
if (result && result.replies) {
|
||||||
|
const replies = result.replies.map(r => r.response);
|
||||||
|
for (const reply of replies) reply.forEach(r => responses.push(r));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await this.doProvisionRequest<ListLinksResponseItem[]>(bridge, "GET", "/_matrix/provision/listlinks/" + inRoomId);
|
||||||
|
if (result) responses = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linked: LinkedChannels = {};
|
||||||
|
for (const response of responses) {
|
||||||
|
if (!response || !response.remote_room_server) continue;
|
||||||
|
|
||||||
|
const network = networksByDomain[response.remote_room_server];
|
||||||
|
if (!network) continue;
|
||||||
|
|
||||||
|
if (!linked[network.id]) linked[network.id] = [];
|
||||||
|
linked[network.id].push({
|
||||||
|
channelName: response.remote_room_channel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return linked;
|
||||||
|
}
|
||||||
|
|
||||||
private async fetchNetworks(bridge: IrcBridgeRecord): Promise<CachedNetwork[]> {
|
private async fetchNetworks(bridge: IrcBridgeRecord): Promise<CachedNetwork[]> {
|
||||||
let responses: QueryNetworksResponse[] = [];
|
let responses: QueryNetworksResponse[] = [];
|
||||||
if (bridge.upstreamId) {
|
if (bridge.upstreamId) {
|
||||||
const result = await this.doUpstreamRequest<ModularIrcQueryNetworksResponse>(bridge, "GET", "/bridges/irc/_matrix/provision/querynetworks");
|
const result = await this.doUpstreamRequest<ModularIrcResponse<QueryNetworksResponse>>(bridge, "GET", "/bridges/irc/_matrix/provision/querynetworks");
|
||||||
if (result && result.replies) responses = result.replies.map(r => r.response);
|
if (result && result.replies) responses = result.replies.map(r => r.response);
|
||||||
} else {
|
} else {
|
||||||
const result = await this.doProvisionRequest<QueryNetworksResponse>(bridge, "GET", "/_matrix/provision/querynetworks");
|
const result = await this.doProvisionRequest<QueryNetworksResponse>(bridge, "GET", "/_matrix/provision/querynetworks");
|
||||||
|
@ -7,4 +7,14 @@ export interface QueryNetworksResponse {
|
|||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListLinksResponseItem {
|
||||||
|
matrix_room_id: string;
|
||||||
|
remote_room_channel: string;
|
||||||
|
remote_room_server: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListOpsResponse {
|
||||||
|
operators: string[];
|
||||||
}
|
}
|
@ -32,13 +32,12 @@ export class BridgeStore {
|
|||||||
return bridge.save();
|
return bridge.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async setBridgeRoomConfig(requestingUserId: string, integrationType: string, inRoomId: string, newConfig: any): Promise<any> {
|
public static async setBridgeRoomConfig(_requestingUserId: string, integrationType: string, _inRoomId: string, _newConfig: any): Promise<any> {
|
||||||
const record = await BridgeRecord.findOne({where: {type: integrationType}});
|
const record = await BridgeRecord.findOne({where: {type: integrationType}});
|
||||||
if (!record) throw new Error("Bridge not found");
|
if (!record) throw new Error("Bridge not found");
|
||||||
|
|
||||||
if (integrationType === "irc") {
|
if (integrationType === "irc") {
|
||||||
const irc = new IrcBridge(requestingUserId);
|
throw new Error("IRC Bridges should be modified with the dedicated API");
|
||||||
return irc.setRoomConfiguration(requestingUserId, inRoomId, newConfig);
|
|
||||||
} else throw new Error("Unsupported bridge");
|
} else throw new Error("Unsupported bridge");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +52,7 @@ export class BridgeStore {
|
|||||||
if (record.type === "irc") {
|
if (record.type === "irc") {
|
||||||
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs
|
||||||
const irc = new IrcBridge(requestingUserId);
|
const irc = new IrcBridge(requestingUserId);
|
||||||
return irc.getRoomConfiguration(requestingUserId, inRoomId);
|
return irc.getRoomConfiguration(inRoomId);
|
||||||
} else return {};
|
} else return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Integration } from "./Integration";
|
import { Integration } from "./Integration";
|
||||||
import BridgeRecord from "../db/models/BridgeRecord";
|
import BridgeRecord from "../db/models/BridgeRecord";
|
||||||
import { AvailableNetworks } from "../bridges/IrcBridge";
|
import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge";
|
||||||
|
|
||||||
export class Bridge extends Integration {
|
export class Bridge extends Integration {
|
||||||
constructor(bridge: BridgeRecord, public config: any) {
|
constructor(bridge: BridgeRecord, public config: any) {
|
||||||
@ -12,17 +12,12 @@ export class Bridge extends Integration {
|
|||||||
argument: null, // not used
|
argument: null, // not used
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// We'll just say we aren't
|
// We'll just say we don't support encryption
|
||||||
this.isEncryptionSupported = false;
|
this.isEncryptionSupported = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IrcBridgeConfiguration {
|
export interface IrcBridgeConfiguration {
|
||||||
availableNetworks: AvailableNetworks;
|
availableNetworks: AvailableNetworks;
|
||||||
links: {
|
links: LinkedChannels;
|
||||||
[networkId: string]: {
|
|
||||||
channelName: string;
|
|
||||||
addedByUserId: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
}
|
@ -1,13 +1,11 @@
|
|||||||
import { QueryNetworksResponse } from "../bridges/models/provision_responses";
|
|
||||||
|
|
||||||
export interface ModularIntegrationInfoResponse {
|
export interface ModularIntegrationInfoResponse {
|
||||||
bot_user_id: string;
|
bot_user_id: string;
|
||||||
integrations?: any[];
|
integrations?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModularIrcQueryNetworksResponse {
|
export interface ModularIrcResponse<T> {
|
||||||
replies: {
|
replies: {
|
||||||
rid: string;
|
rid: string;
|
||||||
response: QueryNetworksResponse;
|
response: T;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
@ -68,6 +68,8 @@ import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component";
|
|||||||
import { AdminIrcApiService } from "./shared/services/admin/admin-irc-api.service";
|
import { AdminIrcApiService } from "./shared/services/admin/admin-irc-api.service";
|
||||||
import { AdminIrcBridgeNetworksComponent } from "./admin/bridges/irc/networks/networks.component";
|
import { AdminIrcBridgeNetworksComponent } from "./admin/bridges/irc/networks/networks.component";
|
||||||
import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-selfhosted/add-selfhosted.component";
|
import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-selfhosted/add-selfhosted.component";
|
||||||
|
import { IrcBridgeConfigComponent } from "./configs/bridge/irc/irc.bridge.component";
|
||||||
|
import { IrcApiService } from "./shared/services/integrations/irc-api.service";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -129,6 +131,7 @@ import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-se
|
|||||||
AdminIrcBridgeComponent,
|
AdminIrcBridgeComponent,
|
||||||
AdminIrcBridgeNetworksComponent,
|
AdminIrcBridgeNetworksComponent,
|
||||||
AdminIrcBridgeAddSelfhostedComponent,
|
AdminIrcBridgeAddSelfhostedComponent,
|
||||||
|
IrcBridgeConfigComponent,
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
@ -144,6 +147,7 @@ import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-se
|
|||||||
AdminNebApiService,
|
AdminNebApiService,
|
||||||
AdminUpstreamApiService,
|
AdminUpstreamApiService,
|
||||||
AdminIrcApiService,
|
AdminIrcApiService,
|
||||||
|
IrcApiService,
|
||||||
{provide: Window, useValue: window},
|
{provide: Window, useValue: window},
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
|
@ -23,6 +23,7 @@ import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.comp
|
|||||||
import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component";
|
import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component";
|
||||||
import { AdminBridgesComponent } from "./admin/bridges/bridges.component";
|
import { AdminBridgesComponent } from "./admin/bridges/bridges.component";
|
||||||
import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component";
|
import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component";
|
||||||
|
import { IrcBridgeConfigComponent } from "./configs/bridge/irc/irc.bridge.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "", component: HomeComponent},
|
{path: "", component: HomeComponent},
|
||||||
@ -142,15 +143,16 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: "bridge",
|
path: "bridge",
|
||||||
// children: [
|
children: [
|
||||||
// {
|
{
|
||||||
// path: "irc",
|
path: "irc",
|
||||||
//
|
component: IrcBridgeConfigComponent,
|
||||||
// }
|
data: {breadcrumb: "IRC Bridge Configuration", name: "IRC Bridge Configuration"},
|
||||||
// ]
|
},
|
||||||
// }
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
89
web/app/configs/bridge/irc/irc.bridge.component.html
Normal file
89
web/app/configs/bridge/irc/irc.bridge.component.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<my-bridge-config [bridgeComponent]="this">
|
||||||
|
<ng-template #bridgeParamsTemplate>
|
||||||
|
<my-ibox [isCollapsible]="true">
|
||||||
|
<h5 class="my-ibox-title">
|
||||||
|
Add an IRC channel
|
||||||
|
</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.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="channelStep === 1">
|
||||||
|
<label class="label-block">
|
||||||
|
Network
|
||||||
|
<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
|
||||||
|
</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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="channelStep === 2">
|
||||||
|
<label class="label-block">
|
||||||
|
Operator
|
||||||
|
<span class="text-muted ">The person selected here will be asked to approve or deny the bridge request.</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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</my-ibox>
|
||||||
|
<my-ibox [isCollapsible]="true">
|
||||||
|
<h5 class="my-ibox-title">
|
||||||
|
IRC Networks
|
||||||
|
</h5>
|
||||||
|
<div class="my-ibox-content">
|
||||||
|
<table class="table table-striped table-condensed table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Channel</th>
|
||||||
|
<th>Network</th>
|
||||||
|
<th class="actions-col">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngIf="getChannels().length === 0">
|
||||||
|
<td colspan="3">No bridged channels</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
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</my-ibox>
|
||||||
|
</ng-template>
|
||||||
|
</my-bridge-config>
|
4
web/app/configs/bridge/irc/irc.bridge.component.scss
Normal file
4
web/app/configs/bridge/irc/irc.bridge.component.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.actions-col {
|
||||||
|
width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
133
web/app/configs/bridge/irc/irc.bridge.component.ts
Normal file
133
web/app/configs/bridge/irc/irc.bridge.component.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { BridgeComponent } from "../bridge.component";
|
||||||
|
import { FE_IrcBridgeAvailableNetworks } from "../../../shared/models/irc";
|
||||||
|
import { IrcApiService } from "../../../shared/services/integrations/irc-api.service";
|
||||||
|
|
||||||
|
interface IrcConfig {
|
||||||
|
availableNetworks: FE_IrcBridgeAvailableNetworks;
|
||||||
|
links: {
|
||||||
|
[networkId: string]: {
|
||||||
|
channelName: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalChannel {
|
||||||
|
name: string;
|
||||||
|
networkId: string;
|
||||||
|
networkName: string;
|
||||||
|
pending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "irc.bridge.component.html",
|
||||||
|
styleUrls: ["irc.bridge.component.scss"],
|
||||||
|
})
|
||||||
|
export class IrcBridgeConfigComponent extends BridgeComponent<IrcConfig> {
|
||||||
|
|
||||||
|
public loadingOps = false;
|
||||||
|
public requestingBridge = false;
|
||||||
|
public channelStep = 1;
|
||||||
|
public networkId: string;
|
||||||
|
public channel = "";
|
||||||
|
public ops: string[];
|
||||||
|
public op: string;
|
||||||
|
public pending: LocalChannel[] = [];
|
||||||
|
|
||||||
|
constructor(private irc: IrcApiService) {
|
||||||
|
super("irc");
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetForm() {
|
||||||
|
this.networkId = this.getNetworks()[0].id;
|
||||||
|
this.channel = "";
|
||||||
|
this.ops = [];
|
||||||
|
this.channelStep = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNetworks(): { id: string, name: 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};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadOps() {
|
||||||
|
if (!this.channel.trim()) {
|
||||||
|
this.toaster.pop("warning", "Please enter a channel name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingOps = true;
|
||||||
|
this.irc.getOperators(this.networkId, this.channel).then(ops => {
|
||||||
|
this.ops = ops;
|
||||||
|
this.op = ops[0];
|
||||||
|
this.loadingOps = false;
|
||||||
|
this.channelStep = 2;
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.loadingOps = false;
|
||||||
|
this.toaster.pop("error", "Error loading channel operators");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async requestBridge() {
|
||||||
|
this.requestingBridge = true;
|
||||||
|
const bridgeUserId = this.bridge.config.availableNetworks[this.networkId].bridgeUserId;
|
||||||
|
|
||||||
|
const memberEvent = await this.scalarClientApi.getMembershipState(this.roomId, bridgeUserId);
|
||||||
|
const isJoined = memberEvent && memberEvent.response && ["join", "invite"].indexOf(memberEvent.response.membership) !== -1;
|
||||||
|
|
||||||
|
if (!isJoined) await this.scalarClientApi.inviteUser(this.roomId, bridgeUserId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.scalarClientApi.setUserPowerLevel(this.roomId, bridgeUserId, 100);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.requestingBridge = false;
|
||||||
|
this.toaster.pop("error", "Failed to make the bridge an administrator", "Please ensure you are an 'Admin' for the room");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.irc.requestLink(this.roomId, this.networkId, this.channel, this.op).then(() => {
|
||||||
|
this.requestingBridge = false;
|
||||||
|
this.pending.push({
|
||||||
|
name: this.channel,
|
||||||
|
networkId: this.networkId,
|
||||||
|
networkName: this.bridge.config.availableNetworks[this.networkId].name,
|
||||||
|
pending: true,
|
||||||
|
});
|
||||||
|
this.resetForm();
|
||||||
|
this.toaster.pop("success", "Link requested!", "The operator selected will have to approve the bridge for it to work");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.requestingBridge = false;
|
||||||
|
this.toaster.pop("error", "Failed to request a link");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getChannels(): LocalChannel[] {
|
||||||
|
const channels: LocalChannel[] = [];
|
||||||
|
this.pending.forEach(p => channels.push(p));
|
||||||
|
|
||||||
|
for (const networkId of Object.keys(this.bridge.config.links)) {
|
||||||
|
const network = this.bridge.config.availableNetworks[networkId];
|
||||||
|
|
||||||
|
for (const channel of this.bridge.config.links[networkId]) {
|
||||||
|
channels.push({
|
||||||
|
networkId: networkId,
|
||||||
|
networkName: network.name,
|
||||||
|
name: channel.channelName,
|
||||||
|
pending: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeChannel(channel: any) {
|
||||||
|
console.log(channel);
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,6 @@ export interface FE_ComplexBot<T> extends FE_Integration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FE_Bridge<T> extends FE_Integration {
|
export interface FE_Bridge<T> extends FE_Integration {
|
||||||
bridgeUserId: string;
|
|
||||||
config: T;
|
config: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,4 +55,10 @@ export interface CanSendEventResponse extends ScalarRoomResponse {
|
|||||||
|
|
||||||
export interface RoomEncryptionStatusResponse extends ScalarRoomResponse {
|
export interface RoomEncryptionStatusResponse extends ScalarRoomResponse {
|
||||||
response: boolean;
|
response: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetPowerLevelResponse extends ScalarRoomResponse {
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
18
web/app/shared/services/integrations/irc-api.service.ts
Normal file
18
web/app/shared/services/integrations/irc-api.service.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Http } from "@angular/http";
|
||||||
|
import { AuthedApi } from "../authed-api";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IrcApiService extends AuthedApi {
|
||||||
|
constructor(http: Http) {
|
||||||
|
super(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOperators(networkId: string, channelNoHash: string): Promise<string[]> {
|
||||||
|
return this.authedGet("/api/v1/dimension/irc/" + networkId + "/channel/" + channelNoHash + "/ops").map(r => r.json()).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public requestLink(roomId: string, networkId: string, channelNoHash: string, op: string): Promise<any> {
|
||||||
|
return this.authedPost("/api/v1/dimension/irc/" + networkId + "/channel/" + channelNoHash + "/link/" + roomId, {op: op}).map(r => r.json()).toPromise();
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,10 @@ import * as randomString from "random-string";
|
|||||||
import {
|
import {
|
||||||
CanSendEventResponse,
|
CanSendEventResponse,
|
||||||
JoinRuleStateResponse,
|
JoinRuleStateResponse,
|
||||||
MembershipStateResponse, RoomEncryptionStatusResponse,
|
MembershipStateResponse,
|
||||||
|
RoomEncryptionStatusResponse,
|
||||||
ScalarSuccessResponse,
|
ScalarSuccessResponse,
|
||||||
|
SetPowerLevelResponse,
|
||||||
WidgetsResponse
|
WidgetsResponse
|
||||||
} from "../../models/server-client-responses";
|
} from "../../models/server-client-responses";
|
||||||
import { EditableWidget } from "../../models/widget";
|
import { EditableWidget } from "../../models/widget";
|
||||||
@ -87,6 +89,14 @@ export class ScalarClientApiService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setUserPowerLevel(roomId: string, userId: string, powerLevel: number): Promise<SetPowerLevelResponse> {
|
||||||
|
return this.callAction("set_bot_power", {
|
||||||
|
room_id: roomId,
|
||||||
|
user_id: userId,
|
||||||
|
level: powerLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private callAction(action, payload): Promise<any> {
|
private callAction(action, payload): Promise<any> {
|
||||||
let requestKey = randomString({length: 20});
|
let requestKey = randomString({length: 20});
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user