mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
More tweaking
This commit is contained in:
parent
c9a01aa0cf
commit
9748fe09cf
42
Cargo.lock
generated
42
Cargo.lock
generated
@ -2,6 +2,28 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b"
|
||||
|
||||
[[package]]
|
||||
name = "contrast"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e2e6885a8c59c03522edaa351a7ad5b6d47ed2632fcfbc0f2b00fcce520eb1"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
@ -39,9 +61,11 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
name = "matrix-github"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"contrast",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"rgb",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@ -83,6 +107,15 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67cf20e0081fea04e044aa4adf74cfea8ddc0324eec2894b1c700f4cafc72a56"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
@ -107,6 +140,15 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27fa03bb1e3e2941f52d4a555a395a72bf79b0a85fbbaab79447050c97d978c"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
|
@ -12,7 +12,9 @@ napi-derive = "1"
|
||||
url = "2"
|
||||
serde_json = "1"
|
||||
serde = "1"
|
||||
serde_derive = "*"
|
||||
serde_derive = "1"
|
||||
contrast = "0"
|
||||
rgb = "0"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "1"
|
@ -35,7 +35,6 @@
|
||||
"@octokit/rest": "^18.10.0",
|
||||
"@octokit/webhooks": "^9.1.2",
|
||||
"axios": "^0.21.1",
|
||||
"contrast-color": "^1.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"ioredis": "^4.26.0",
|
||||
|
@ -7,7 +7,7 @@
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
import { BridgeConfig, GitLabInstance } from "./Config/Config";
|
||||
import { GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection } from "./Connections";
|
||||
import { GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection } from "./Connections";
|
||||
import { GenericHookConnection } from "./Connections/GenericHook";
|
||||
import { JiraProjectConnection } from "./Connections/JiraProject";
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
@ -183,7 +183,7 @@ export class ConnectionManager {
|
||||
owner = owner.toLowerCase();
|
||||
repo = repo.toLowerCase();
|
||||
return this.connections.filter(
|
||||
(c) => (
|
||||
c => (
|
||||
c instanceof GitHubDiscussionConnection &&
|
||||
c.owner === owner &&
|
||||
c.repo === repo &&
|
||||
@ -192,6 +192,15 @@ export class ConnectionManager {
|
||||
) as GitHubDiscussionConnection[];
|
||||
}
|
||||
|
||||
public getForGitHubProject(projectId: number): GitHubProjectConnection[] {
|
||||
return this.connections.filter(
|
||||
c => (
|
||||
c instanceof GitHubProjectConnection &&
|
||||
c.projectId === projectId
|
||||
)
|
||||
) as GitHubProjectConnection[];
|
||||
}
|
||||
|
||||
public getConnectionsForGitLabIssueWebhook(repoHome: string, issueId: number) {
|
||||
if (!this.config.gitlab) {
|
||||
throw Error('GitLab configuration missing, cannot handle note');
|
||||
|
@ -48,6 +48,10 @@ export class GitHubProjectConnection implements IConnection {
|
||||
return new GitHubProjectConnection(roomId, as, state, project.url)
|
||||
}
|
||||
|
||||
get projectId() {
|
||||
return this.state.project_id;
|
||||
}
|
||||
|
||||
constructor(public readonly roomId: string,
|
||||
as: Appservice,
|
||||
private state: GitHubProjectConnectionState,
|
||||
|
@ -289,11 +289,11 @@ export class GitHubRepoConnection implements IConnection {
|
||||
const orgRepoName = event.repository.full_name;
|
||||
|
||||
const content = emoji.emojify(`${event.issue.user?.login} created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`);
|
||||
const { labelsHtml, labelsStr } = FormatUtil.formatLabels(event.issue.labels);
|
||||
const labels = FormatUtil.formatLabels(event.issue.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined })));
|
||||
await this.as.botIntent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content + (labelsStr.length > 0 ? ` with labels ${labelsStr}`: ""),
|
||||
formatted_body: md.renderInline(content) + (labelsHtml.length > 0 ? ` with labels ${labelsHtml}`: ""),
|
||||
body: content + (labels.plain.length > 0 ? ` with labels ${labels.plain}`: ""),
|
||||
formatted_body: md.renderInline(content) + (labels.html.length > 0 ? ` with labels ${labels.html}`: ""),
|
||||
format: "org.matrix.custom.html",
|
||||
// TODO: Fix types.
|
||||
...FormatUtil.getPartialBodyForIssue(event.repository, event.issue as any),
|
||||
@ -358,11 +358,11 @@ export class GitHubRepoConnection implements IConnection {
|
||||
const orgRepoName = event.repository.full_name;
|
||||
const verb = event.pull_request.draft ? 'drafted' : 'opened';
|
||||
const content = emoji.emojify(`**${event.sender.login}** ${verb} a new PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"`);
|
||||
const { labelsHtml, labelsStr } = FormatUtil.formatLabels(event.pull_request.labels);
|
||||
const labels = FormatUtil.formatLabels(event.pull_request.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined })));
|
||||
await this.as.botIntent.sendEvent(this.roomId, {
|
||||
msgtype: "m.notice",
|
||||
body: content + (labelsStr.length > 0 ? ` with labels ${labelsStr}`: ""),
|
||||
formatted_body: md.renderInline(content) + (labelsHtml.length > 0 ? ` with labels ${labelsHtml}`: ""),
|
||||
body: content + (labels.plain.length > 0 ? ` with labels ${labels}`: ""),
|
||||
formatted_body: md.renderInline(content) + (labels.html.length > 0 ? ` with labels ${labels.html}`: ""),
|
||||
format: "org.matrix.custom.html",
|
||||
// TODO: Fix types.
|
||||
...FormatUtil.getPartialBodyForIssue(event.repository, event.pull_request as any),
|
||||
|
@ -6,7 +6,7 @@ import { MessageSenderClient } from "../MatrixSender"
|
||||
import { JiraIssueEvent } from "../Jira/WebhookTypes";
|
||||
import { FormatUtil } from "../FormatUtil";
|
||||
import markdownit from "markdown-it";
|
||||
import { generateJiraWebLinkFromIssue } from "../Jira/Utils";
|
||||
import { generateJiraWebLinkFromIssue } from "../Jira";
|
||||
|
||||
type JiraAllowedEventsNames = "issue.created";
|
||||
const JiraAllowedEvents: JiraAllowedEventsNames[] = ["issue.created"];
|
||||
|
@ -1,16 +1,106 @@
|
||||
|
||||
use napi::{CallContext, Env, Error as NapiError, JsObject, JsUnknown};
|
||||
|
||||
use napi::{CallContext, Env, Error as NapiError, JsObject, JsUnknown, Status};
|
||||
use std::fmt::Write;
|
||||
use crate::Jira::types::{JiraIssue, JiraIssueLight};
|
||||
use crate::Jira;
|
||||
use rgb::RGB;
|
||||
use contrast;
|
||||
|
||||
#[derive(Serialize, Debug, Deserialize)]
|
||||
struct IssueLabelDetail {
|
||||
color: Option<String>,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
pub fn get_module(env: Env) -> Result<JsObject, NapiError> {
|
||||
let mut root_module = env.create_object()?;
|
||||
root_module.create_named_method("get_partial_body_for_jira_issue", get_partial_body_for_jira_issue)?;
|
||||
root_module.create_named_method("format_labels", format_labels)?;
|
||||
Ok(root_module)
|
||||
}
|
||||
|
||||
fn parse_rgb(input_color: String) -> Result<rgb::RGB8, NapiError> {
|
||||
let chunk_size;
|
||||
let color;
|
||||
if input_color.starts_with('#') {
|
||||
let mut chars = input_color.chars();
|
||||
chars.next();
|
||||
color = String::from_iter(chars);
|
||||
} else {
|
||||
color = input_color;
|
||||
}
|
||||
match color.len() {
|
||||
6 => {
|
||||
chunk_size = 2;
|
||||
},
|
||||
3 => {
|
||||
chunk_size = 1;
|
||||
},
|
||||
_ => {
|
||||
return Err(NapiError::new(Status::InvalidArg, format!("color '{}' is invalid", color).to_string()));
|
||||
}
|
||||
}
|
||||
let rgb = color.as_bytes().chunks(chunk_size).map(std::str::from_utf8).collect::<Result<Vec<&str>, _>>().unwrap();
|
||||
let r = u8::from_str_radix(rgb[0], 16).unwrap();
|
||||
let g = u8::from_str_radix(rgb[1], 16).unwrap();
|
||||
let b = u8::from_str_radix(rgb[2], 16).unwrap();
|
||||
Ok(RGB::new(r,g,b))
|
||||
}
|
||||
|
||||
#[js_function(1)]
|
||||
pub fn format_labels(ctx: CallContext) -> Result<JsObject, NapiError> {
|
||||
let array: JsObject = ctx.get::<JsObject>(0)?;
|
||||
if array.is_array()? != true {
|
||||
return Err(NapiError::new(Status::InvalidArg, "labels is not an array".to_string()));
|
||||
}
|
||||
let mut plain = String::new();
|
||||
let mut html = String::new();
|
||||
let mut i = 0;
|
||||
while array.has_element(i)? {
|
||||
let label: IssueLabelDetail = ctx.env.from_js_value(array.get_element_unchecked::<JsUnknown>(i)?)?;
|
||||
|
||||
if i != 0 {
|
||||
plain.push_str(", ");
|
||||
html.push_str(" ");
|
||||
}
|
||||
plain.push_str(&label.name);
|
||||
|
||||
// HTML
|
||||
html.push_str("<span");
|
||||
match label.color {
|
||||
Some(color) => {
|
||||
write!(html, " data-mx-bg-color=\"{}\"", color).unwrap();
|
||||
// Determine the constrast
|
||||
let color_rgb = parse_rgb(color)?;
|
||||
let contrast_color;
|
||||
if contrast::contrast::<u8, f32>(color_rgb, RGB::new(0,0,0)) > 4.5 {
|
||||
contrast_color = "#000000";
|
||||
} else {
|
||||
contrast_color = "#FFFFFF";
|
||||
}
|
||||
write!(html, " data-mx-color=\"{}\"", contrast_color).unwrap();
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
match label.description {
|
||||
Some(description) => {
|
||||
write!(html, " title=\"{}\"", description).unwrap();
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
html.push_str(">");
|
||||
html.push_str(&label.name);
|
||||
html.push_str("</span>");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let mut body = ctx.env.create_object()?;
|
||||
body.set_named_property("plain", ctx.env.create_string_from_std(plain)?)?;
|
||||
body.set_named_property("html", ctx.env.create_string_from_std(html)?)?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Generate a URL for a given Jira Issue object.
|
||||
#[js_function(1)]
|
||||
pub fn get_partial_body_for_jira_issue(ctx: CallContext) -> Result<JsObject, NapiError> {
|
||||
|
@ -23,6 +23,12 @@ interface IMinimalIssue {
|
||||
pull_request?: any;
|
||||
}
|
||||
|
||||
export interface ILabel {
|
||||
color?: string,
|
||||
name: string,
|
||||
description?: string
|
||||
}
|
||||
|
||||
export class FormatUtil {
|
||||
public static formatIssueRoomName(issue: IMinimalIssue) {
|
||||
const orgRepoName = issue.repository_url.substr("https://api.github.com/repos/".length);
|
||||
@ -86,23 +92,8 @@ export class FormatUtil {
|
||||
return f;
|
||||
}
|
||||
|
||||
public static formatLabels(labels: Array<{color?: string|null, name?: string, description?: string|null}|string> = []) {
|
||||
const labelsHtml = labels.map((label: {color?: string|null, name?: string, description?: string|null}|string) => {
|
||||
if (typeof(label) === "string") {
|
||||
return `<span>${label}</span>`;
|
||||
}
|
||||
const fontColor = contrastColor(label.color);
|
||||
return `<span title="${label.description}" data-mx-color="${fontColor}" data-mx-bg-color="#${label.color}">${label.name}</span>`
|
||||
}
|
||||
|
||||
).join(" ") || "";
|
||||
const labelsStr = labels.map((label: {name?: string}|string) =>
|
||||
typeof(label) === "string" ? label : label.name
|
||||
).join(", ") || "";
|
||||
return {
|
||||
labelsStr,
|
||||
labelsHtml,
|
||||
}
|
||||
public static formatLabels(labels: ILabel[] = []): { plain: string, html: string } {
|
||||
return format_util.format_labels(labels);
|
||||
}
|
||||
|
||||
public static getPartialBodyForJiraIssue(issue: JiraIssue) {
|
||||
|
@ -3,20 +3,17 @@ import { Appservice, IAppserviceRegistration, RichRepliesPreprocessor, IRichRepl
|
||||
import { BridgeConfig, GitLabInstance } from "./Config/Config";
|
||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
import { ConnectionManager } from "./ConnectionManager";
|
||||
import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GenericHookConnection } from "./Connections/GenericHook";
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { GitHubIssueConnection } from "./Connections/GithubIssue";
|
||||
import { GitHubProjectConnection } from "./Connections/GithubProject";
|
||||
import { GitHubRepoConnection } from "./Connections/GithubRepo";
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
import { GitLabIssueConnection } from "./Connections/GitlabIssue";
|
||||
import { GitLabRepoConnection } from "./Connections/GitlabRepo";
|
||||
import { IBridgeStorageProvider } from "./Stores/StorageProvider";
|
||||
import { IConnection, GitHubDiscussionSpace, GitHubDiscussionConnection, GitHubUserSpace } from "./Connections";
|
||||
import { IGitLabWebhookIssueStateEvent, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
||||
import { JiraIssueEvent } from "./Jira/WebhookTypes";
|
||||
import { JiraProjectConnection } from "./Connections/JiraProject";
|
||||
import { MatrixEvent, MatrixMemberContent, MatrixMessageContent } from "./MatrixEvent";
|
||||
import { MemoryStorageProvider } from "./Stores/MemoryStorageProvider";
|
||||
import { MessageQueue, createMessageQueue } from "./MessageQueue/MessageQueue";
|
||||
@ -31,8 +28,6 @@ import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher"
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
|
||||
import LogWrapper from "./LogWrapper";
|
||||
import { ConnectionManager } from "./ConnectionManager";
|
||||
|
||||
const log = new LogWrapper("GithubBridge");
|
||||
|
||||
export class GithubBridge {
|
||||
@ -857,9 +852,13 @@ export class GithubBridge {
|
||||
);
|
||||
adminRoom.on("settings.changed", this.onAdminRoomSettingsChanged.bind(this));
|
||||
adminRoom.on("open.project", async (project: ProjectsGetResponseData) => {
|
||||
const connection = await GitHubProjectConnection.onOpenProject(project, this.as, adminRoom.userId);
|
||||
// TODO: Surely we only do this if we don't have one already?
|
||||
this.connectionManager?.push(connection);
|
||||
const [connection] = this.connectionManager?.getForGitHubProject(project.id) || [];
|
||||
if (!connection) {
|
||||
const connection = await GitHubProjectConnection.onOpenProject(project, this.as, adminRoom.userId);
|
||||
this.connectionManager?.push(connection);
|
||||
} else {
|
||||
await this.as.botClient.inviteUser(adminRoom.userId, connection.roomId);
|
||||
}
|
||||
});
|
||||
// adminRoom.on("open.discussion", async (owner: string, repo: string, discussions: Discussion) => {
|
||||
// const connection = await GitHubDiscussionConnection.createDiscussionRoom(
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { ILabel } from "./FormatUtil";
|
||||
import { JiraIssue } from "./Jira/Types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@ -13,6 +14,7 @@ try {
|
||||
|
||||
interface FormatUtil {
|
||||
get_partial_body_for_jira_issue: (issue: JiraIssue) => Record<string, unknown>
|
||||
format_labels: (labels: ILabel[]) => { plain: string, html: string }
|
||||
}
|
||||
|
||||
interface JiraModule {
|
||||
|
@ -43,21 +43,49 @@ const SIMPLE_JIRA_ISSUE = {
|
||||
} as JiraIssue;
|
||||
|
||||
describe("FormatUtilTest", () => {
|
||||
it("correctly formats a repo room name", () => {
|
||||
it("should correctly formats a repo room name", () => {
|
||||
expect(FormatUtil.formatRepoRoomName(SIMPLE_REPO)).to.equal(
|
||||
"evilcorp/lab: A simple description",
|
||||
);
|
||||
});
|
||||
it("correctly formats a issue room name", () => {
|
||||
it("should correctly formats a issue room name", () => {
|
||||
expect(FormatUtil.formatIssueRoomName(SIMPLE_ISSUE)).to.equal(
|
||||
"evilcorp/lab#123: A simple title",
|
||||
);
|
||||
});
|
||||
it("correctly formats a room topic", () => {
|
||||
it("should correctly formats a room topic", () => {
|
||||
expect(FormatUtil.formatRoomTopic(SIMPLE_ISSUE)).to.equal(
|
||||
"Status: open | https://github.com/evilcorp/lab/issues/123",
|
||||
);
|
||||
});
|
||||
it("should correctly format one simple label", () => {
|
||||
expect(FormatUtil.formatLabels([{name: "foo"}])).to.deep.equal({
|
||||
plain: "foo",
|
||||
html: "<span>foo</span>"
|
||||
});
|
||||
});
|
||||
it("should correctly format many simple labels", () => {
|
||||
expect(FormatUtil.formatLabels([{name: "foo"},{name: "bar"}])).to.deep.equal({
|
||||
plain: "foo, bar",
|
||||
html: "<span>foo</span> <span>bar</span>"
|
||||
});
|
||||
});
|
||||
it("should correctly format one detailed label", () => {
|
||||
expect(FormatUtil.formatLabels([{name: "foo", color: '#FFFFFF', description: 'My label'}])).to.deep.equal({
|
||||
plain: "foo",
|
||||
html: "<span data-mx-bg-color=\"#FFFFFF\" data-mx-color=\"#000000\" title=\"My label\">foo</span>"
|
||||
});
|
||||
});
|
||||
it("should correctly format many detailed labels", () => {
|
||||
expect(FormatUtil.formatLabels([
|
||||
{name: "foo", color: '#FFFFFF', description: 'My label'},
|
||||
{name: "bar", color: '#AACCEE', description: 'My other label'},
|
||||
])).to.deep.equal({
|
||||
plain: "foo, bar",
|
||||
html: "<span data-mx-bg-color=\"#FFFFFF\" data-mx-color=\"#000000\" title=\"My label\">foo</span> "
|
||||
+ "<span data-mx-bg-color=\"#AACCEE\" data-mx-color=\"#000000\" title=\"My other label\">bar</span>"
|
||||
},);
|
||||
});
|
||||
it("should correctly format a JIRA issue", () => {
|
||||
expect(FormatUtil.getPartialBodyForJiraIssue(SIMPLE_JIRA_ISSUE)).to.deep.equal({
|
||||
"external_url": "http://example-api.url.com/browse/TEST-001",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { expect } from "chai";
|
||||
import { generateJiraWebLinkFromIssue } from "../../src/Jira/Utils";
|
||||
import { generateJiraWebLinkFromIssue } from "../../src/Jira";
|
||||
|
||||
describe("Jira", () => {
|
||||
describe("Utils", () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user