From 71b1d9e347b7dff8b93d653538bb3b100772184e Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sun, 13 Sep 2020 20:06:19 +0800 Subject: [PATCH] feat(github): support github.recent command --- packages/plugin-github/src/events.ts | 87 +++++++++++++--------- packages/plugin-github/src/index.ts | 54 +++++++++----- packages/plugin-github/src/server.ts | 5 +- packages/plugin-github/tests/index.spec.ts | 16 +++- 4 files changed, 104 insertions(+), 58 deletions(-) diff --git a/packages/plugin-github/src/events.ts b/packages/plugin-github/src/events.ts index 1d28126cd0..b385788a07 100644 --- a/packages/plugin-github/src/events.ts +++ b/packages/plugin-github/src/events.ts @@ -93,7 +93,8 @@ export const defaultEvents: EventConfig = { }, } -export interface ReplyPayloads { +export interface EventData { + message?: string link?: string react?: string reply?: [url: string, params?: Record] @@ -105,7 +106,7 @@ export interface ReplyPayloads { } type Payload = GetWebhookPayloadTypeFromEvent['payload'] -type EventHandler = (payload: Payload) => [message: string, replies?: ReplyPayloads] +type EventHandler = (payload: Payload) => EventData export function addListeners(on: (event: T, handler: EventHandler) => void) { function formatMarkdown(source: string) { @@ -114,12 +115,12 @@ export function addListeners(on: (event: T, handler: E .replace(/\n\s*\n/g, '\n') } - interface CommantReplyPayloads extends ReplyPayloads { + interface CommentReplyPayloads extends EventData { padding?: number[] } type CommentEvent = 'commit_comment' | 'issue_comment' | 'pull_request_review_comment' - type CommantHandler = (payload: Payload) => [target: string, replies: CommantReplyPayloads] + type CommantHandler = (payload: Payload) => [target: string, replies: CommentReplyPayloads] function onComment(event: E, handler: CommantHandler) { on(event as CommentEvent, (payload) => { @@ -128,12 +129,13 @@ export function addListeners(on: (event: T, handler: E const [target, replies] = handler(payload) if (payload.action === 'deleted') { - return [`[GitHub] ${user.login} deleted a comment on ${target}`] + return { message: `${user.login} deleted a comment on ${target}` } } const index = html_url.indexOf('#') const operation = payload.action === 'created' ? 'commented' : 'edited a comment' - return [`[GitHub] ${user.login} ${operation} on ${target}\n${formatMarkdown(body)}`, { + return { + message: `${user.login} ${operation} on ${target}\n${formatMarkdown(body)}`, link: html_url, react: url + `/reactions`, shot: { @@ -142,7 +144,7 @@ export function addListeners(on: (event: T, handler: E padding: replies.padding, }, ...replies, - }] + } }) } @@ -157,7 +159,9 @@ export function addListeners(on: (event: T, handler: E on('fork', ({ repository, sender, forkee }) => { const { full_name, forks_count } = repository - return [`[GitHub] ${sender.login} forked ${full_name} to ${forkee.full_name} (total ${forks_count} forks)`] + return { + message: `${sender.login} forked ${full_name} to ${forkee.full_name} (total ${forks_count} forks)`, + } }) onComment('issue_comment', ({ issue, repository }) => { @@ -175,15 +179,16 @@ export function addListeners(on: (event: T, handler: E const { user, url, html_url, comments_url, title, body, number } = issue if (user.type === 'Bot') return - return [[ - `[GitHub] ${user.login} opened an issue ${full_name}#${number}`, - `Title: ${title}`, - formatMarkdown(body), - ].join('\n'), { + return { + message: [ + `${user.login} opened an issue ${full_name}#${number}`, + `Title: ${title}`, + formatMarkdown(body), + ].join('\n'), link: html_url, react: url + `/reactions`, reply: [comments_url], - }] + } }) on('issues.closed', ({ repository, issue }) => { @@ -191,11 +196,12 @@ export function addListeners(on: (event: T, handler: E const { user, url, html_url, comments_url, title, number } = issue if (user.type === 'Bot') return - return [`[GitHub] ${user.login} closed issue ${full_name}#${number}\n${title}`, { + return { + message: `${user.login} closed issue ${full_name}#${number}\n${title}`, link: html_url, react: url + `/reactions`, reply: [comments_url], - }] + } }) onComment('pull_request_review_comment', ({ repository, comment, pull_request }) => { @@ -214,10 +220,14 @@ export function addListeners(on: (event: T, handler: E const { user, html_url, body } = review if (user.type === 'Bot') return - return [[ - `[GitHub] ${user.login} reviewed pull request ${full_name}#${number}`, - formatMarkdown(body), - ].join('\n'), { link: html_url, reply: [comments_url] }] + return { + message: [ + `${user.login} reviewed pull request ${full_name}#${number}`, + formatMarkdown(body), + ].join('\n'), + link: html_url, + reply: [comments_url], + } }) on('pull_request.closed', ({ repository, pull_request, sender }) => { @@ -225,11 +235,12 @@ export function addListeners(on: (event: T, handler: E const { html_url, issue_url, comments_url, title, number, merged } = pull_request const type = merged ? 'merged' : 'closed' - return [`[GitHub] ${sender.login} ${type} pull request ${full_name}#${number}\n${title}`, { + return { + message: `${sender.login} ${type} pull request ${full_name}#${number}\n${title}`, link: html_url, react: issue_url + '/reactions', reply: [comments_url], - }] + } }) on('pull_request.opened', ({ repository, pull_request }) => { @@ -240,15 +251,16 @@ export function addListeners(on: (event: T, handler: E const prefix = new RegExp(`^${owner.login}:`) const baseLabel = base.label.replace(prefix, '') const headLabel = head.label.replace(prefix, '') - return [[ - `[GitHub] ${user.login} opened a pull request ${full_name}#${number} (${baseLabel} <- ${headLabel})`, - `Title: ${title}`, - formatMarkdown(body), - ].join('\n'), { + return { + message: [ + `${user.login} opened a pull request ${full_name}#${number} (${baseLabel} <- ${headLabel})`, + `Title: ${title}`, + formatMarkdown(body), + ].join('\n'), link: html_url, react: issue_url + '/reactions', reply: [comments_url], - }] + } }) on('push', ({ compare, pusher, commits, repository, ref, after }) => { @@ -259,17 +271,24 @@ export function addListeners(on: (event: T, handler: E // use short form for tag releases if (ref.startsWith('refs/tags')) { - return [`[GitHub] ${pusher.name} published tag ${full_name}@${ref.slice(10)}`] + return { + message: `${pusher.name} published tag ${full_name}@${ref.slice(10)}`, + } } - return [[ - `[GitHub] ${pusher.name} pushed to ${full_name}:${ref.replace(/^refs\/heads\//, '')}`, - ...commits.map(c => `[${c.id.slice(0, 6)}] ${formatMarkdown(c.message)}`), - ].join('\n'), { link: compare }] + return { + message: [ + `${pusher.name} pushed to ${full_name}:${ref.replace(/^refs\/heads\//, '')}`, + ...commits.map(c => `[${c.id.slice(0, 6)}] ${formatMarkdown(c.message)}`), + ].join('\n'), + link: compare, + } }) on('star.created', ({ repository, sender }) => { const { full_name, stargazers_count } = repository - return [`[GitHub] ${sender.login} starred ${full_name} (total ${stargazers_count} stargazers)`] + return { + message: `${sender.login} starred ${full_name} (total ${stargazers_count} stargazers)`, + } }) } diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index f1a8397898..68e5974c6e 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -4,7 +4,7 @@ import { Context, Session } from 'koishi-core' import { camelize, CQCode, defineProperty, Logger, Time } from 'koishi-utils' import { encode } from 'querystring' -import { addListeners, defaultEvents, ReplyPayloads } from './events' +import { addListeners, defaultEvents, EventData } from './events' import { Config, GitHub } from './server' import {} from 'koishi-plugin-puppeteer' @@ -17,12 +17,13 @@ declare module 'koishi-core/dist/app' { } type ReplyHandlers = { - [K in keyof ReplyPayloads]: (payload: ReplyPayloads[K], session: Session, message: string) => Promise + [K in keyof EventData]: (payload: EventData[K], session: Session, message: string) => Promise } const defaultOptions: Config = { secret: '', - prefix: '.', + replyPrefix: '.', + messagePrefix: '[GitHub] ', webhook: '/github/webhook', authorize: '/github/authorize', replyTimeout: Time.hour, @@ -35,7 +36,7 @@ export const name = 'github' export function apply(ctx: Context, config: Config = {}) { config = { ...defaultOptions, ...config } const { app, database, router } = ctx - const { appId, prefix, redirect, webhook } = config + const { appId, replyPrefix, redirect, webhook } = config const github = new GitHub(config) defineProperty(app, 'github', github) @@ -55,7 +56,9 @@ export function apply(ctx: Context, config: Config = {}) { return ctx.status = 200 }) - ctx.command('github ', '授权 GitHub 功能') + ctx.command('github', 'GitHub 相关功能') + + ctx.command('github.authorize ', 'GitHub 授权') .action(async ({ session }, user) => { if (!user) return '请输入用户名。' const url = 'https://github.com/login/oauth/authorize?' + encode({ @@ -68,6 +71,16 @@ export function apply(ctx: Context, config: Config = {}) { return '请点击下面的链接继续操作:\n' + url }) + ctx.command('github.recent', '查看最近的通知') + .action(async () => { + const output = Object.entries(history).slice(0, 10).map(([messageId, payload]) => { + const [brief] = payload.message.split('\n', 1) + return `${messageId}. ${brief}` + }) + if (!output.length) return '最近没有 GitHub 通知。' + return output.join('\n') + }) + const reactions = ['+1', '-1', 'laugh', 'confused', 'heart', 'hooray', 'rocket', 'eyes'] function formatReply(source: string) { @@ -114,7 +127,7 @@ export function apply(ctx: Context, config: Config = {}) { }, } - const interactions: Record = {} + const history: Record = {} router.post(webhook, (ctx, next) => { // workaround @octokit/webhooks for koa @@ -124,7 +137,7 @@ export function apply(ctx: Context, config: Config = {}) { }) ctx.on('before-attach-user', (session, fields) => { - if (interactions[session.$reply]) { + if (history[int32ToHex6(session.$reply)]) { fields.add('ghAccessToken') fields.add('ghRefreshToken') } @@ -132,13 +145,13 @@ export function apply(ctx: Context, config: Config = {}) { ctx.middleware((session, next) => { const body = session.$parsed - const payloads = interactions[session.$reply] + const payloads = history[int32ToHex6(session.$reply)] if (!body || !payloads) return next() let name: string, message: string - if (body.startsWith(prefix)) { - name = body.split(' ', 1)[0].slice(prefix.length) - message = body.slice(prefix.length + name.length).trim() + if (body.startsWith(replyPrefix)) { + name = body.split(' ', 1)[0].slice(replyPrefix.length) + message = body.slice(replyPrefix.length + name.length).trim() } else { name = reactions.includes(body) ? 'react' : 'reply' message = body @@ -173,19 +186,24 @@ export function apply(ctx: Context, config: Config = {}) { if (!result) return // step 4: broadcast message - const [message, replies] = result - const messageIds = await ctx.broadcast(groupIds, message) - if (!replies) return + const messageIds = await ctx.broadcast(groupIds, config.messagePrefix + result.message) + const hexIds = messageIds.map(int32ToHex6) // step 5: save message ids for interactions - for (const id of messageIds) { - interactions[id] = replies + for (const id of hexIds) { + history[id] = result } + setTimeout(() => { - for (const id of messageIds) { - delete interactions[id] + for (const id of hexIds) { + delete history[id] } }, config.replyTimeout) }) }) } + +function int32ToHex6(source: number) { + if (source < 0) source -= 1 << 31 + return source.toString(16).padStart(8, '0').slice(2) +} diff --git a/packages/plugin-github/src/server.ts b/packages/plugin-github/src/server.ts index 975faabc32..235ddc6c9d 100644 --- a/packages/plugin-github/src/server.ts +++ b/packages/plugin-github/src/server.ts @@ -22,7 +22,8 @@ export interface Config { secret?: string webhook?: string authorize?: string - prefix?: string + replyPrefix?: string + messagePrefix?: string appId?: string appSecret?: string redirect?: string @@ -86,7 +87,7 @@ export class GitHub extends Webhooks { await session.$send(message) const name = await session.$prompt(this.config.promptTimeout) if (!name) return session.$send('输入超时。') - return session.$execute({ command: 'github', args: [name] }) + return session.$execute({ command: 'github.authorize', args: [name] }) } async post(options: PostOptions) { diff --git a/packages/plugin-github/tests/index.spec.ts b/packages/plugin-github/tests/index.spec.ts index 779c184480..f5eab1571c 100644 --- a/packages/plugin-github/tests/index.spec.ts +++ b/packages/plugin-github/tests/index.spec.ts @@ -74,13 +74,17 @@ describe('GitHub Plugin', () => { await expect(app.server.post('/github/webhook', {})).to.eventually.have.property('code', 400) }) - it('github command', async () => { - await session1.shouldReply('github', '请输入用户名。') - await session1.shouldReply('github satori', /^请点击下面的链接继续操作:/) + it('github.authorize', async () => { + await session1.shouldReply('github.authorize', '请输入用户名。') + await session1.shouldReply('github.authorize satori', /^请点击下面的链接继续操作:/) + }) + + it('github.recent', async () => { + await session1.shouldReply('github.recent', '最近没有 GitHub 通知。') }) }) - let counter = 10000 + let counter = 0x100000 const idMap: Record = {} describe('Webhook Events', () => { @@ -172,5 +176,9 @@ describe('GitHub Plugin', () => { expect(unauthorized.mock.calls).to.have.length(1) expect(notFound.mock.calls).to.have.length(1) }) + + it('github.recent', async () => { + await session1.shouldReply('github.recent', /^100001\./) + }) }) })