Skip to content

Commit

Permalink
feat(github): support github.recent command
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Sep 13, 2020
1 parent a056933 commit 71b1d9e
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 58 deletions.
87 changes: 53 additions & 34 deletions packages/plugin-github/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>]
Expand All @@ -105,7 +106,7 @@ export interface ReplyPayloads {
}

type Payload<T extends EventNames.All> = GetWebhookPayloadTypeFromEvent<T, unknown>['payload']
type EventHandler<T extends EventNames.All> = (payload: Payload<T>) => [message: string, replies?: ReplyPayloads]
type EventHandler<T extends EventNames.All> = (payload: Payload<T>) => EventData

export function addListeners(on: <T extends EventNames.All>(event: T, handler: EventHandler<T>) => void) {
function formatMarkdown(source: string) {
Expand All @@ -114,12 +115,12 @@ export function addListeners(on: <T extends EventNames.All>(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<E extends CommentEvent> = (payload: Payload<E>) => [target: string, replies: CommantReplyPayloads]
type CommantHandler<E extends CommentEvent> = (payload: Payload<E>) => [target: string, replies: CommentReplyPayloads]

function onComment<E extends CommentEvent>(event: E, handler: CommantHandler<E>) {
on(event as CommentEvent, (payload) => {
Expand All @@ -128,12 +129,13 @@ export function addListeners(on: <T extends EventNames.All>(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: {
Expand All @@ -142,7 +144,7 @@ export function addListeners(on: <T extends EventNames.All>(event: T, handler: E
padding: replies.padding,
},
...replies,
}]
}
})
}

Expand All @@ -157,7 +159,9 @@ export function addListeners(on: <T extends EventNames.All>(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 }) => {
Expand All @@ -175,27 +179,29 @@ export function addListeners(on: <T extends EventNames.All>(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 }) => {
const { full_name } = repository
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 }) => {
Expand All @@ -214,22 +220,27 @@ export function addListeners(on: <T extends EventNames.All>(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 }) => {
const { full_name } = repository
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 }) => {
Expand All @@ -240,15 +251,16 @@ export function addListeners(on: <T extends EventNames.All>(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 }) => {
Expand All @@ -259,17 +271,24 @@ export function addListeners(on: <T extends EventNames.All>(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)`,
}
})
}
54 changes: 36 additions & 18 deletions packages/plugin-github/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -17,12 +17,13 @@ declare module 'koishi-core/dist/app' {
}

type ReplyHandlers = {
[K in keyof ReplyPayloads]: (payload: ReplyPayloads[K], session: Session, message: string) => Promise<void>
[K in keyof EventData]: (payload: EventData[K], session: Session, message: string) => Promise<void>
}

const defaultOptions: Config = {
secret: '',
prefix: '.',
replyPrefix: '.',
messagePrefix: '[GitHub] ',
webhook: '/github/webhook',
authorize: '/github/authorize',
replyTimeout: Time.hour,
Expand All @@ -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)
Expand All @@ -55,7 +56,9 @@ export function apply(ctx: Context, config: Config = {}) {
return ctx.status = 200
})

ctx.command('github <user>', '授权 GitHub 功能')
ctx.command('github', 'GitHub 相关功能')

ctx.command('github.authorize <user>', 'GitHub 授权')
.action(async ({ session }, user) => {
if (!user) return '请输入用户名。'
const url = 'https://github.com/login/oauth/authorize?' + encode({
Expand All @@ -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) {
Expand Down Expand Up @@ -114,7 +127,7 @@ export function apply(ctx: Context, config: Config = {}) {
},
}

const interactions: Record<number, ReplyPayloads> = {}
const history: Record<string, EventData> = {}

router.post(webhook, (ctx, next) => {
// workaround @octokit/webhooks for koa
Expand All @@ -124,21 +137,21 @@ 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')
}
})

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
Expand Down Expand Up @@ -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)
}
5 changes: 3 additions & 2 deletions packages/plugin-github/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 12 additions & 4 deletions packages/plugin-github/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number> = {}

describe('Webhook Events', () => {
Expand Down Expand Up @@ -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\./)
})
})
})

0 comments on commit 71b1d9e

Please sign in to comment.