Add support for threading events in GithubRepo

This commit is contained in:
Half-Shot 2022-04-19 11:55:02 +01:00
parent a766967a62
commit eba27c25ad
7 changed files with 118 additions and 20 deletions

View File

@ -789,7 +789,7 @@ export class Bridge {
try { try {
await ( await (
new SetupConnection( new SetupConnection(
roomId, this.as, this.tokenStore, this.config, roomId, this.as, this.tokenStore, this.storage, this.config,
this.getOrCreateAdminRoom.bind(this), this.getOrCreateAdminRoom.bind(this),
this.github, this.github,
) )

View File

@ -96,7 +96,7 @@ export class ConnectionManager {
if (!this.config.checkPermission(userId, "github", BridgePermissionLevel.manageConnections)) { if (!this.config.checkPermission(userId, "github", BridgePermissionLevel.manageConnections)) {
throw new ApiError('User is not permitted to provision connections for GitHub', ErrCode.ForbiddenUser); throw new ApiError('User is not permitted to provision connections for GitHub', ErrCode.ForbiddenUser);
} }
const res = await GitHubRepoConnection.provisionConnection(roomId, userId, data, this.as, this.tokenStore, this.github, this.config.github); const res = await GitHubRepoConnection.provisionConnection(roomId, userId, data, this.as, this.tokenStore, this.github, this.config.github, this.storage);
await this.as.botIntent.underlyingClient.sendStateEvent(roomId, GitHubRepoConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent); await this.as.botIntent.underlyingClient.sendStateEvent(roomId, GitHubRepoConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent);
this.push(res.connection); this.push(res.connection);
return res.connection; return res.connection;
@ -145,7 +145,7 @@ export class ConnectionManager {
throw Error('GitHub is not configured'); throw Error('GitHub is not configured');
} }
this.assertStateAllowed(state, "github"); this.assertStateAllowed(state, "github");
return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey, this.github, this.config.github); return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, this.storage, state.stateKey, this.github, this.config.github);
} }
if (GitHubDiscussionConnection.EventTypes.includes(state.type)) { if (GitHubDiscussionConnection.EventTypes.includes(state.type)) {

View File

@ -21,6 +21,7 @@ import { BridgeConfigGitHub } from "../Config/Config";
import { ApiError, ErrCode } from "../api"; import { ApiError, ErrCode } from "../api";
import { PermissionCheckFn } from "."; import { PermissionCheckFn } from ".";
import { MinimalGitHubIssue, MinimalGitHubRepo } from "../libRs"; import { MinimalGitHubIssue, MinimalGitHubRepo } from "../libRs";
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
const log = new LogWrapper("GitHubRepoConnection"); const log = new LogWrapper("GitHubRepoConnection");
const md = new markdown(); const md = new markdown();
@ -48,6 +49,7 @@ export interface GitHubRepoConnectionOptions extends IConnectionState {
newIssue?: { newIssue?: {
labels: string[]; labels: string[];
}; };
useThreads?: boolean;
} }
export interface GitHubRepoConnectionState extends GitHubRepoConnectionOptions { export interface GitHubRepoConnectionState extends GitHubRepoConnectionOptions {
org: string; org: string;
@ -150,7 +152,7 @@ function validateState(state: Record<string, unknown>): GitHubRepoConnectionStat
*/ */
export class GitHubRepoConnection extends CommandConnection implements IConnection { export class GitHubRepoConnection extends CommandConnection implements IConnection {
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, as: Appservice, static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, as: Appservice,
tokenStore: UserTokenStore, githubInstance: GithubInstance, config: BridgeConfigGitHub) { tokenStore: UserTokenStore, githubInstance: GithubInstance, config: BridgeConfigGitHub, store: IBridgeStorageProvider) {
const validData = validateState(data); const validData = validateState(data);
const octokit = await tokenStore.getOctokitForUser(userId); const octokit = await tokenStore.getOctokitForUser(userId);
if (!octokit) { if (!octokit) {
@ -183,7 +185,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
const stateEventKey = `${validData.org}/${validData.repo}`; const stateEventKey = `${validData.org}/${validData.repo}`;
return { return {
stateEventContent: validData, stateEventContent: validData,
connection: new GitHubRepoConnection(roomId, as, validData, tokenStore, stateEventKey, githubInstance, config), connection: new GitHubRepoConnection(roomId, as, validData, tokenStore, store, stateEventKey, githubInstance, config),
} }
} }
@ -281,6 +283,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
private readonly as: Appservice, private readonly as: Appservice,
private state: GitHubRepoConnectionState, private state: GitHubRepoConnectionState,
private readonly tokenStore: UserTokenStore, private readonly tokenStore: UserTokenStore,
private readonly store: IBridgeStorageProvider,
stateKey: string, stateKey: string,
private readonly githubInstance: GithubInstance, private readonly githubInstance: GithubInstance,
private readonly config: BridgeConfigGitHub, private readonly config: BridgeConfigGitHub,
@ -297,6 +300,32 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
); );
} }
protected async setThreadForRemoteId(eventId: string, remoteId: string|number) {
// Store regardless of configuration, in case the user wants to turn it on.
await this.store.setEventIdForRemoteId(`${this.connectionId}/${remoteId}`, eventId);
log.debug(`Stored thread eventId for ${remoteId} as ${eventId}`);
}
protected async getThreadForRemoteId(remoteId: string|number, renderInTimeline = false): Promise<Record<string, unknown>|undefined> {
const parentEventId = this.state.useThreads && await this.store.getEventIdForRemoteId(`${this.connectionId}/${remoteId}`);
log.debug(`Found ${parentEventId || "no eventId"} for ${remoteId}`);
if (!parentEventId) {
return;
}
return {
"m.relates_to": {
rel_type: renderInTimeline ? undefined : "m.thread",
event_id: parentEventId,
// Needed to prevent clients from showing these as actual replies
is_falling_back: true,
"m.in_reply_to": {
event_id: parentEventId,
}
},
}
}
public get hotlinkIssues() { public get hotlinkIssues() {
const cfg = this.config.defaultOptions?.hotlinkIssues || this.state.hotlinkIssues; const cfg = this.config.defaultOptions?.hotlinkIssues || this.state.hotlinkIssues;
if (cfg === false) { if (cfg === false) {
@ -582,13 +611,14 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
} }
const content = emoji.emojify(message); const content = emoji.emojify(message);
const labels = FormatUtil.formatLabels(event.issue.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined }))); 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, { const eventId = await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice", msgtype: "m.notice",
body: content + (labels.plain.length > 0 ? ` with labels ${labels.plain}`: ""), body: content + (labels.plain.length > 0 ? ` with labels ${labels.plain}`: ""),
formatted_body: md.renderInline(content) + (labels.html.length > 0 ? ` with labels ${labels.html}`: ""), formatted_body: md.renderInline(content) + (labels.html.length > 0 ? ` with labels ${labels.html}`: ""),
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue), ...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue),
}); });
await this.setThreadForRemoteId(eventId, event.issue.id);
} }
public async onIssueStateChange(event: IssuesEditedEvent|IssuesReopenedEvent|IssuesClosedEvent) { public async onIssueStateChange(event: IssuesEditedEvent|IssuesReopenedEvent|IssuesClosedEvent) {
@ -628,13 +658,18 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
} }
} }
const content = `**${event.sender.login}** ${state} issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"${withComment}`; const content = `**${event.sender.login}** ${state} issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"${withComment}`;
await this.as.botIntent.sendEvent(this.roomId, { const thread = await this.getThreadForRemoteId(event.issue.id, true);
const eventId = await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice", msgtype: "m.notice",
body: content, body: content,
formatted_body: md.renderInline(content), formatted_body: md.renderInline(content),
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
...thread,
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue), ...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue),
}); });
if (!thread) {
this.setThreadForRemoteId(eventId, event.issue.id);
}
} }
public async onIssueEdited(event: IssuesEditedEvent) { public async onIssueEdited(event: IssuesEditedEvent) {
@ -647,13 +682,18 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
log.info(`onIssueEdited ${this.roomId} ${this.org}/${this.repo} #${event.issue.number}`); log.info(`onIssueEdited ${this.roomId} ${this.org}/${this.repo} #${event.issue.number}`);
const orgRepoName = event.repository.full_name; const orgRepoName = event.repository.full_name;
const content = `**${event.sender.login}** edited issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`; const content = `**${event.sender.login}** edited issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`;
await this.as.botIntent.sendEvent(this.roomId, { const thread = await this.getThreadForRemoteId(event.issue.id);
const eventId = await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice", msgtype: "m.notice",
body: content, body: content,
formatted_body: md.renderInline(content), formatted_body: md.renderInline(content),
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
...thread,
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue), ...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue),
}); });
if (!thread) {
this.setThreadForRemoteId(eventId, event.issue.id);
}
} }
public async onIssueLabeled(event: IssuesLabeledEvent) { public async onIssueLabeled(event: IssuesLabeledEvent) {
@ -678,12 +718,15 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
const orgRepoName = event.repository.full_name; const orgRepoName = event.repository.full_name;
const {plain, html} = FormatUtil.formatLabels(event.issue.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined }))); const {plain, html} = FormatUtil.formatLabels(event.issue.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined })));
const content = `**${event.sender.login}** labeled issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`; const content = `**${event.sender.login}** labeled issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`;
this.as.botIntent.sendEvent(this.roomId, { this.getThreadForRemoteId(event.issue.id).then((thread) => {
msgtype: "m.notice", return this.as.botIntent.sendEvent(this.roomId, {
body: content + (plain.length > 0 ? ` with labels ${plain}`: ""), msgtype: "m.notice",
formatted_body: md.renderInline(content) + (html.length > 0 ? ` with labels ${html}`: ""), body: content + (plain.length > 0 ? ` with labels ${plain}`: ""),
format: "org.matrix.custom.html", formatted_body: md.renderInline(content) + (html.length > 0 ? ` with labels ${html}`: ""),
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue), format: "org.matrix.custom.html",
...thread,
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.issue),
})
}).catch(ex => { }).catch(ex => {
log.error('Failed to send onIssueLabeled message', ex); log.error('Failed to send onIssueLabeled message', ex);
}); });
@ -736,14 +779,19 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
} }
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 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 labels = FormatUtil.formatLabels(event.pull_request.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined }))); 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, { const thread = await this.getThreadForRemoteId(event.pull_request.id);
const eventId = await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice", msgtype: "m.notice",
body: content + (labels.plain.length > 0 ? ` with labels ${labels}`: "") + diffContent, body: content + (labels.plain.length > 0 ? ` with labels ${labels}`: "") + diffContent,
formatted_body: md.renderInline(content) + (labels.html.length > 0 ? ` with labels ${labels.html}`: "") + diffContentHtml, formatted_body: md.renderInline(content) + (labels.html.length > 0 ? ` with labels ${labels.html}`: "") + diffContentHtml,
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
...thread,
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request), ...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request),
...FormatUtil.getPartialBodyForGitHubPR(event.repository, event.pull_request), ...FormatUtil.getPartialBodyForGitHubPR(event.repository, event.pull_request),
}); });
if (!thread) {
this.setThreadForRemoteId(eventId, event.pull_request.id);
}
} }
public async onPRReadyForReview(event: PullRequestReadyForReviewEvent) { public async onPRReadyForReview(event: PullRequestReadyForReviewEvent) {
@ -759,14 +807,18 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
} }
const orgRepoName = event.repository.full_name; const orgRepoName = event.repository.full_name;
const content = emoji.emojify(`**${event.sender.login}** has marked [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) as ready to review "${event.pull_request.title}"`); const content = emoji.emojify(`**${event.sender.login}** has marked [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) as ready to review "${event.pull_request.title}"`);
await this.as.botIntent.sendEvent(this.roomId, { const thread = await this.getThreadForRemoteId(event.pull_request.id, true);
const eventId = await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice", msgtype: "m.notice",
body: content, body: content,
formatted_body: md.renderInline(content), formatted_body: md.renderInline(content),
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
// TODO: Fix types. ...thread,
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request), ...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request),
}); });
if (!thread) {
this.setThreadForRemoteId(eventId, event.pull_request.id);
}
} }
public async onPRReviewed(event: PullRequestReviewSubmittedEvent) { public async onPRReviewed(event: PullRequestReviewSubmittedEvent) {
@ -792,14 +844,18 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
return; return;
} }
const content = emoji.emojify(`**${event.sender.login}** ${emojiForReview} ${event.review.state.toLowerCase()} [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) "${event.pull_request.title}"`); const content = emoji.emojify(`**${event.sender.login}** ${emojiForReview} ${event.review.state.toLowerCase()} [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) "${event.pull_request.title}"`);
await this.as.botIntent.sendEvent(this.roomId, { const thread = await this.getThreadForRemoteId(event.pull_request.id, true);
const eventId = await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice", msgtype: "m.notice",
body: content, body: content,
formatted_body: md.renderInline(content), formatted_body: md.renderInline(content),
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
// TODO: Fix types. ...thread,
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request), ...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request),
}); });
if (!thread) {
this.setThreadForRemoteId(eventId, event.pull_request.id);
}
} }
public async onPRClosed(event: PullRequestClosedEvent) { public async onPRClosed(event: PullRequestClosedEvent) {
@ -838,11 +894,13 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
} }
const content = emoji.emojify(`**${event.sender.login}** ${verb} PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"${withComment}`); const content = emoji.emojify(`**${event.sender.login}** ${verb} PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"${withComment}`);
const thread = await this.getThreadForRemoteId(event.pull_request.id, true);
await this.as.botIntent.sendEvent(this.roomId, { await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice", msgtype: "m.notice",
body: content, body: content,
formatted_body: md.renderInline(content), formatted_body: md.renderInline(content),
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
...thread,
// TODO: Fix types. // TODO: Fix types.
...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request), ...FormatUtil.getPartialBodyForGithubIssue(event.repository, event.pull_request),
}); });

View File

@ -13,6 +13,7 @@ import { FigmaFileConnection } from "./FigmaFileConnection";
import { URL } from "url"; import { URL } from "url";
import { SetupWidget } from "../Widgets/SetupWidget"; import { SetupWidget } from "../Widgets/SetupWidget";
import { AdminRoom } from "../AdminRoom"; import { AdminRoom } from "../AdminRoom";
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
const md = new markdown(); const md = new markdown();
/** /**
@ -27,6 +28,7 @@ export class SetupConnection extends CommandConnection {
constructor(public readonly roomId: string, constructor(public readonly roomId: string,
private readonly as: Appservice, private readonly as: Appservice,
private readonly tokenStore: UserTokenStore, private readonly tokenStore: UserTokenStore,
private readonly store: IBridgeStorageProvider,
private readonly config: BridgeConfig, private readonly config: BridgeConfig,
private readonly getOrCreateAdminRoom: (userId: string) => Promise<AdminRoom>, private readonly getOrCreateAdminRoom: (userId: string) => Promise<AdminRoom>,
private readonly githubInstance?: GithubInstance,) { private readonly githubInstance?: GithubInstance,) {
@ -73,7 +75,7 @@ export class SetupConnection extends CommandConnection {
throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid."); throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid.");
} }
const [, org, repo] = urlParts; const [, org, repo] = urlParts;
const res = await GitHubRepoConnection.provisionConnection(this.roomId, userId, {org, repo}, this.as, this.tokenStore, this.githubInstance, this.config.github); const res = await GitHubRepoConnection.provisionConnection(this.roomId, userId, {org, repo}, this.as, this.tokenStore, this.githubInstance, this.config.github, this.store);
await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, url, res.stateEventContent); await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, url, res.stateEventContent);
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`); await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`);
} }

View File

@ -2,6 +2,7 @@ import { MemoryStorageProvider as MSP } from "matrix-bot-sdk";
import { IBridgeStorageProvider } from "./StorageProvider"; import { IBridgeStorageProvider } from "./StorageProvider";
import { IssuesGetResponseData } from "../Github/Types"; import { IssuesGetResponseData } from "../Github/Types";
import { ProvisionSession } from "matrix-appservice-bridge"; import { ProvisionSession } from "matrix-appservice-bridge";
import QuickLRU from "@alloc/quick-lru";
export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider { export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider {
private issues: Map<string, IssuesGetResponseData> = new Map(); private issues: Map<string, IssuesGetResponseData> = new Map();
@ -10,6 +11,9 @@ export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider
private figmaCommentIds: Map<string, string> = new Map(); private figmaCommentIds: Map<string, string> = new Map();
private widgetSessions: Map<string, ProvisionSession> = new Map(); private widgetSessions: Map<string, ProvisionSession> = new Map();
// Set to a suitably large, but bounded size.
private eventIdForRemoteId = new QuickLRU<string, string>({ maxSize: 5000 });
constructor() { constructor() {
super(); super();
} }
@ -54,16 +58,26 @@ export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider
public async getSessionForToken(token: string) { public async getSessionForToken(token: string) {
return this.widgetSessions.get(token) || null; return this.widgetSessions.get(token) || null;
} }
public async createSession(session: ProvisionSession) { public async createSession(session: ProvisionSession) {
this.widgetSessions.set(session.token, session); this.widgetSessions.set(session.token, session);
} }
public async deleteSession(token: string) { public async deleteSession(token: string) {
this.widgetSessions.delete(token); this.widgetSessions.delete(token);
} }
public async deleteAllSessions(userId: string) { public async deleteAllSessions(userId: string) {
[...this.widgetSessions.values()] [...this.widgetSessions.values()]
.filter(s => s.userId === userId) .filter(s => s.userId === userId)
.forEach(s => this.widgetSessions.delete(s.token)); .forEach(s => this.widgetSessions.delete(s.token));
} }
public async getEventIdForRemoteId(remoteId: string) {
return this.eventIdForRemoteId.get(remoteId) ?? null;
}
public async setEventIdForRemoteId(remoteId: string, eventId: string) {
this.eventIdForRemoteId.set(remoteId, eventId);
}
} }

View File

@ -15,9 +15,12 @@ const GH_ISSUES_KEY = "gh.issues";
const GH_ISSUES_LAST_COMMENT_KEY = "gh.issues.last_comment"; const GH_ISSUES_LAST_COMMENT_KEY = "gh.issues.last_comment";
const GH_ISSUES_REVIEW_DATA_KEY = "gh.issues.review_data"; const GH_ISSUES_REVIEW_DATA_KEY = "gh.issues.review_data";
const FIGMA_EVENT_COMMENT_ID = "figma.comment_event_id"; const FIGMA_EVENT_COMMENT_ID = "figma.comment_event_id";
const REMOTE_EVENTS_HASH = "remote_events";
const REMOTE_EVENTS_LRU = "remote_events_lru";
const COMPLETED_TRANSACTIONS_EXPIRE_AFTER = 24 * 60 * 60; // 24 hours const COMPLETED_TRANSACTIONS_EXPIRE_AFTER = 24 * 60 * 60; // 24 hours
const ISSUES_EXPIRE_AFTER = 7 * 24 * 60 * 60; // 7 days const ISSUES_EXPIRE_AFTER = 7 * 24 * 60 * 60; // 7 days
const ISSUES_LAST_COMMENT_EXPIRE_AFTER = 14 * 24 * 60 * 60; // 7 days const ISSUES_LAST_COMMENT_EXPIRE_AFTER = 14 * 24 * 60 * 60; // 7 days
const REMOTE_EVENTS_MAX_SIZE = 5000;
const WIDGET_TOKENS = "widgets.tokens."; const WIDGET_TOKENS = "widgets.tokens.";
@ -155,4 +158,23 @@ export class RedisStorageProvider implements IBridgeStorageProvider {
token = await this.redis.spop(`${WIDGET_USER_TOKENS}${userId}`); token = await this.redis.spop(`${WIDGET_USER_TOKENS}${userId}`);
} }
} }
async getEventIdForRemoteId(remoteId: string): Promise<string|null> {
const result = await this.redis.hget(REMOTE_EVENTS_HASH, remoteId);
if (result) {
await this.redis.zadd(REMOTE_EVENTS_LRU, Date.now(), remoteId);
}
return result;
}
async setEventIdForRemoteId(remoteId: string, eventId: string): Promise<void> {
await this.redis.hset(REMOTE_EVENTS_HASH, remoteId, eventId);
await this.redis.zadd(REMOTE_EVENTS_LRU, Date.now(), remoteId);
const lruCount = await this.redis.zcount(REMOTE_EVENTS_LRU, "-inf", "+inf");
if (lruCount <= REMOTE_EVENTS_MAX_SIZE) {
return;
}
const popped = await this.redis.zpopmin(REMOTE_EVENTS_LRU, lruCount - REMOTE_EVENTS_MAX_SIZE);
await this.redis.zrem(REMOTE_EVENTS_HASH, popped);
}
} }

View File

@ -11,4 +11,6 @@ export interface IBridgeStorageProvider extends IAppserviceStorageProvider, ISto
getPRReviewData(repo: string, issueNumber: string, scope?: string): Promise<any|null>; getPRReviewData(repo: string, issueNumber: string, scope?: string): Promise<any|null>;
setFigmaCommentEventId(roomId: string, figmaCommentId: string, eventId: string): Promise<void>; setFigmaCommentEventId(roomId: string, figmaCommentId: string, eventId: string): Promise<void>;
getFigmaCommentEventId(roomId: string, figmaCommentId: string): Promise<string|null>; getFigmaCommentEventId(roomId: string, figmaCommentId: string): Promise<string|null>;
setEventIdForRemoteId(remoteId: string, eventId: string): Promise<void>;
getEventIdForRemoteId(remoteId: string): Promise<string|null>;
} }