From e0879123d7f32109dd1c465c1a1e598003ca8ccb Mon Sep 17 00:00:00 2001 From: meteorlxy Date: Mon, 30 Dec 2019 20:27:55 +0800 Subject: [PATCH 1/4] build: add gitea to code spell check --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index e37fba58..c7b98ae2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,7 @@ "Preact", "bitbucket", "deletable", + "gitea", "gitee", "github", "gitlab", From 98a141620501a40c292d72b6ffe363a670649cae Mon Sep 17 00:00:00 2001 From: meteorlxy Date: Tue, 31 Dec 2019 15:48:25 +0800 Subject: [PATCH 2/4] feat(api-gitea-v1): add gitea api v1 package --- packages/@vssue/api-gitea-v1/.gitignore | 1 + packages/@vssue/api-gitea-v1/README.md | 12 + packages/@vssue/api-gitea-v1/package.json | 35 ++ packages/@vssue/api-gitea-v1/src/index.ts | 563 +++++++++++++++++++++ packages/@vssue/api-gitea-v1/src/utils.ts | 51 ++ packages/@vssue/api-gitea-v1/tsconfig.json | 10 + 6 files changed, 672 insertions(+) create mode 100644 packages/@vssue/api-gitea-v1/.gitignore create mode 100644 packages/@vssue/api-gitea-v1/README.md create mode 100644 packages/@vssue/api-gitea-v1/package.json create mode 100644 packages/@vssue/api-gitea-v1/src/index.ts create mode 100644 packages/@vssue/api-gitea-v1/src/utils.ts create mode 100644 packages/@vssue/api-gitea-v1/tsconfig.json diff --git a/packages/@vssue/api-gitea-v1/.gitignore b/packages/@vssue/api-gitea-v1/.gitignore new file mode 100644 index 00000000..c3af8579 --- /dev/null +++ b/packages/@vssue/api-gitea-v1/.gitignore @@ -0,0 +1 @@ +lib/ diff --git a/packages/@vssue/api-gitea-v1/README.md b/packages/@vssue/api-gitea-v1/README.md new file mode 100644 index 00000000..051bcd6d --- /dev/null +++ b/packages/@vssue/api-gitea-v1/README.md @@ -0,0 +1,12 @@ +# @vssue/api-gitea-v1 + +> Vssue API for Gitea V1 + +[__Live Demo and Docs__](https://vssue.js.org) + +[__Github Repo__](https://github.com/meteorlxy/vssue) + +## Features + +- Comments sortable: `false` +- Comments reactions: `true` diff --git a/packages/@vssue/api-gitea-v1/package.json b/packages/@vssue/api-gitea-v1/package.json new file mode 100644 index 00000000..3a23d9fc --- /dev/null +++ b/packages/@vssue/api-gitea-v1/package.json @@ -0,0 +1,35 @@ +{ + "name": "@vssue/api-gitea-v1", + "version": "1.2.2", + "description": "Vssue api for gitea v1", + "keywords": [ + "comment", + "issue", + "vue" + ], + "homepage": "https://vssue.js.org", + "bugs": { + "url": "https://github.com/meteorlxy/vssue/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/meteorlxy/vssue.git" + }, + "license": "MIT", + "author": "meteorlxy ", + "files": [ + "lib" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "rimraf lib types && tsc -p tsconfig.json" + }, + "dependencies": { + "@vssue/utils": "1.1.1", + "axios": "^0.18.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@vssue/api-gitea-v1/src/index.ts b/packages/@vssue/api-gitea-v1/src/index.ts new file mode 100644 index 00000000..af17c361 --- /dev/null +++ b/packages/@vssue/api-gitea-v1/src/index.ts @@ -0,0 +1,563 @@ +import { VssueAPI } from 'vssue' + +import axios, { + AxiosInstance, + AxiosRequestConfig, +} from 'axios' + +import { + buildURL, + concatURL, + getCleanURL, + parseQuery, +} from '@vssue/utils' + +import { + normalizeUser, + normalizeIssue, + normalizeComment, + normalizeReactions, + mapReactionName, +} from './utils' + +/** + * Gitea API V1 + * + * @see https://docs.gitea.io/en-us/oauth2-provider/ + * @see https://docs.gitea.io/en-us/api-usage + * @see https://gitea.com/api/swagger + */ +export default class GiteaV1 implements VssueAPI.Instance { + baseURL: string + owner: string + repo: string + labels: Array + clientId: string + clientSecret: string + state: string + proxy: string | ((url: string) => string) + $http: AxiosInstance + + constructor ({ + baseURL = 'https://gitea.com', + owner, + repo, + labels, + clientId, + clientSecret, + state, + proxy, + }: VssueAPI.Options) { + /* istanbul ignore if */ + if (typeof clientSecret === 'undefined' || typeof proxy === 'undefined') { + throw new Error('clientSecret and proxy is required for Gitea V1') + } + this.baseURL = baseURL + this.owner = owner + this.repo = repo + this.labels = labels + + this.clientId = clientId + this.clientSecret = clientSecret + this.state = state + this.proxy = proxy + + this.$http = axios.create({ + baseURL: concatURL(baseURL, 'api/v1'), + headers: { + 'Accept': 'application/json', + }, + }) + } + + /** + * The platform api info + */ + get platform (): VssueAPI.Platform { + return { + name: 'Gitea', + link: this.baseURL, + version: 'v1', + meta: { + reactable: true, + sortable: false, + }, + } + } + + /** + * Redirect to the authorization page of platform. + * + * @see https://docs.gitea.io/en-us/oauth2-provider/ + */ + redirectAuth (): void { + window.location.href = buildURL(concatURL(this.baseURL, 'login/oauth/authorize'), { + client_id: this.clientId, + redirect_uri: window.location.href, + response_type: 'code', + state: this.state, + }) + } + + /** + * Handle authorization. + * + * @see https://docs.gitea.io/en-us/oauth2-provider/ + * + * @remarks + * If the `code` and `state` exist in the query, and the `state` matches, remove them from query, and try to get the access token. + */ + async handleAuth (): Promise { + const query = parseQuery(window.location.search) + if (query.code) { + if (query.state !== this.state) { + return null + } + // the `code` from gitea is uri encoded + // typically includes an encoded `=` -> `%3D` + const code = decodeURIComponent(query.code) + delete query.code + delete query.state + const replaceURL = buildURL(getCleanURL(window.location.href), query) + window.location.hash + window.history.replaceState(null, '', replaceURL) + const accessToken = await this.getAccessToken({ code }) + return accessToken + } + return null + } + + /** + * Get user access token via `code` + * + * @see https://docs.gitea.io/en-us/oauth2-provider/ + */ + async getAccessToken ({ + code, + }: { + code: string + }): Promise { + const originalURL = concatURL(this.baseURL, 'login/oauth/access_token') + const proxyURL = typeof this.proxy === 'function' + ? this.proxy(originalURL) + : this.proxy + const { data } = await this.$http.post(proxyURL, { + 'client_id': this.clientId, + 'client_secret': this.clientSecret, + 'code': code, + 'grant_type': 'authorization_code', + 'redirect_uri': window.location.href, + }) + return data.access_token + } + + /** + * Get the logged-in user with access token. + * + * @see https://gitea.com/api/swagger#/user/userGetCurrent + */ + async getUser ({ + accessToken, + }: { + accessToken: VssueAPI.AccessToken + }): Promise { + const { data } = await this.$http.get('user', { + headers: { 'Authorization': `bearer ${accessToken}` }, + }) + return normalizeUser(data, this.baseURL) + } + + /** + * Get issue of this page according to the issue id or the issue title + * + * @see https://gitea.com/api/swagger#/issue/issueListIssues + * @see https://gitea.com/api/swagger#/issue/issueGetIssue + */ + async getIssue ({ + accessToken, + issueId, + issueTitle, + }: { + accessToken: VssueAPI.AccessToken + issueId?: string | number + issueTitle?: string + }): Promise { + const options: AxiosRequestConfig = {} + + if (accessToken) { + options.headers = { + 'Authorization': `bearer ${accessToken}`, + } + } + + if (issueId) { + try { + options.params = { + // to avoid caching + timestamp: Date.now(), + } + const { data } = await this.$http.get(`repos/${this.owner}/${this.repo}/issues/${issueId}`, options) + return normalizeIssue(data, this.baseURL, this.owner, this.repo) + } catch (e) { + if (e.response && e.response.status === 404) { + return null + } else { + throw e + } + } + } else { + /** + * Gitea only supports using label ids to get issues + */ + const allLabels = await this.getLabels({ accessToken }) + const labels = this.labels + .filter(label => allLabels.find(item => item.name === label)) + .map(label => allLabels.find(item => item.name === label).id) + + options.params = { + labels, + q: issueTitle, + // to avoid caching + timestamp: Date.now(), + } + + const { data } = await this.$http.get(`repos/${this.owner}/${this.repo}/issues`, options) + const issue = data + .map( + item => normalizeIssue(item, this.baseURL, this.owner, this.repo) + ) + .find(item => item.title === issueTitle) + return issue || null + } + } + + /** + * Create a new issue + * + * @see https://gitea.com/api/swagger#/issue/issueCreateIssue + */ + async postIssue ({ + accessToken, + title, + content, + }: { + accessToken: VssueAPI.AccessToken + title: string + content: string + }): Promise { + /** + * Gitea only supports using label ids to create issue + */ + const labels = await Promise.all( + this.labels.map(label => this.postLabel( + { + accessToken, + label, + } + )) + ) + + const { data } = await this.$http.post(`repos/${this.owner}/${this.repo}/issues`, { + title, + body: content, + labels, + }, { + headers: { 'Authorization': `bearer ${accessToken}` }, + }) + return normalizeIssue(data, this.baseURL, this.owner, this.repo) + } + + /** + * Get comments of this page according to the issue id + * + * @see https://gitea.com/api/swagger#/issue/issueGetComments + */ + async getComments ({ + accessToken, + issueId, + query: { + page = 1, + perPage = 10, + sort = 'desc', + } = {}, + }: { + accessToken: VssueAPI.AccessToken + issueId: string | number + query?: Partial + }): Promise { + const options: AxiosRequestConfig = { + params: { + // to avoid caching + timestamp: Date.now(), + }, + } + if (accessToken) { + options.headers = { + 'Authorization': `bearer ${accessToken}`, + } + } + const response = await this.$http.get( + `repos/${this.owner}/${this.repo}/issues/${issueId}/comments`, + options + ) + const commentsRaw = response.data + + // gitea api v1 should get html content and reactions by other api + const getCommentsMeta: Array> = [] + + for (const comment of commentsRaw) { + getCommentsMeta.push((async () => { + comment.body_html = await this.getMarkdownContent({ + accessToken: accessToken, + contentRaw: comment.body, + }) + })()) + getCommentsMeta.push((async () => { + comment.reactions = await this.getCommentReactions({ + accessToken: accessToken, + issueId: issueId, + commentId: comment.id, + }) + })()) + } + + await Promise.all(getCommentsMeta) + + return { + count: commentsRaw.length, + // gitea api v1 does not support pagination for now + // so the `page` and `perPage` are fake data + page: 1, + perPage: 50, + data: commentsRaw.map(item => normalizeComment(item, this.baseURL)), + } + } + + /** + * Create a new comment + * + * @see https://gitea.com/api/swagger#/issue/issueCreateComment + */ + async postComment ({ + accessToken, + issueId, + content, + }: { + accessToken: VssueAPI.AccessToken + issueId: string | number + content: string + }): Promise { + const { data } = await this.$http.post( + `repos/${this.owner}/${this.repo}/issues/${issueId}/comments`, + { + 'body': content, + }, { + headers: { 'Authorization': `bearer ${accessToken}` }, + } + ) + data.body_html = await this.getMarkdownContent({ + accessToken: accessToken, + contentRaw: data.body, + }) + return normalizeComment(data, this.baseURL) + } + + /** + * Edit a comment + * + * @see https://gitea.com/api/swagger#/issue/issueEditCommentDeprecated + */ + async putComment ({ + accessToken, + issueId, + commentId, + content, + }: { + accessToken: VssueAPI.AccessToken + issueId: string | number + commentId: string | number + content: string + }): Promise { + const { data } = await this.$http.patch( + `repos/${this.owner}/${this.repo}/issues/comments/${commentId}`, + { + 'body': content, + }, + { + headers: { 'Authorization': `bearer ${accessToken}` }, + } + ) + data.body_html = await this.getMarkdownContent({ + accessToken: accessToken, + contentRaw: data.body, + }) + return normalizeComment(data, this.baseURL) + } + + /** + * Delete a comment + * + * @see https://gitea.com/api/swagger#/issue/issueDeleteCommentDeprecated + */ + async deleteComment ({ + accessToken, + issueId, + commentId, + }: { + accessToken: VssueAPI.AccessToken + issueId: string | number + commentId: string | number + }): Promise { + const { status } = await this.$http.delete( + `repos/${this.owner}/${this.repo}/issues/comments/${commentId}`, + { + headers: { 'Authorization': `bearer ${accessToken}` }, + } + ) + return status === 204 + } + + /** + * Get reactions of a comment + * + * @see https://gitea.com/api/swagger#/issue/issueGetCommentReactions + */ + async getCommentReactions ({ + accessToken, + issueId, + commentId, + }: { + accessToken: VssueAPI.AccessToken + issueId: string | number + commentId: string | number + }): Promise { + const options: AxiosRequestConfig = {} + if (accessToken) { + options.headers = { + 'Authorization': `bearer ${accessToken}`, + } + } + const { data } = await this.$http.get( + `repos/${this.owner}/${this.repo}/issues/comments/${commentId}/reactions`, + options, + ) + // data is possibly be `null` + return normalizeReactions(data || []) + } + + /** + * Create a new reaction of a comment + * + * @see https://gitea.com/api/swagger#/issue/issuePostCommentReaction + */ + async postCommentReaction ({ + issueId, + commentId, + reaction, + accessToken, + }: { + accessToken: VssueAPI.AccessToken + issueId: string | number + commentId: string | number + reaction: keyof VssueAPI.Reactions + }): Promise { + try { + const response = await this.$http.post( + `repos/${this.owner}/${this.repo}/issues/comments/${commentId}/reactions`, + { + content: mapReactionName(reaction), + }, + { + headers: { 'Authorization': `bearer ${accessToken}` }, + } + ) + return response.status === 201 + } catch (e) { + // https://github.com/go-gitea/gitea/issues/9544 + if (e.response && e.response.status === 500) { + return false + } else { + throw e + } + } + } + + /** + * Get labels + * + * @see https://gitea.com/api/swagger#/issue/issueListLabels + */ + async getLabels ({ + accessToken, + }: { + accessToken: VssueAPI.AccessToken + }): Promise> { + const options: AxiosRequestConfig = {} + if (accessToken) { + options.headers = { + 'Authorization': `bearer ${accessToken}`, + } + } + const { data } = await this.$http.get( + `repos/${this.owner}/${this.repo}/labels`, + options + ) + return data || [] + } + + /** + * Create label + * + * @see https://gitea.com/api/swagger#/issue/issueCreateLabel + */ + async postLabel ({ + accessToken, + label, + color = '#3eaf7c', + description, + }: { + accessToken: VssueAPI.AccessToken + label: string + color?: string + description?: string + }): Promise { + const { data } = await this.$http.post( + `repos/${this.owner}/${this.repo}/labels`, + { + name: label, + color, + description, + }, + { + headers: { 'Authorization': `bearer ${accessToken}` }, + } + ) + return data.id + } + + /** + * Get the parse HTML of markdown content + * + * @see https://gitea.com/api/swagger#/miscellaneous/renderMarkdown + */ + async getMarkdownContent ({ + accessToken, + contentRaw, + }: { + accessToken?: VssueAPI.AccessToken + contentRaw: string + }): Promise { + const options: AxiosRequestConfig = {} + if (accessToken) { + options.headers = { + 'Authorization': `bearer ${accessToken}`, + } + } + const { data } = await this.$http.post(`markdown`, { + 'Context': `${this.owner}/${this.repo}`, + 'Mode': 'gfm', + 'Text': contentRaw, + 'Wiki': false, + }, options) + return data + } +} diff --git a/packages/@vssue/api-gitea-v1/src/utils.ts b/packages/@vssue/api-gitea-v1/src/utils.ts new file mode 100644 index 00000000..cccf0de2 --- /dev/null +++ b/packages/@vssue/api-gitea-v1/src/utils.ts @@ -0,0 +1,51 @@ +import { VssueAPI } from 'vssue' +import { concatURL } from '@vssue/utils' + +export function normalizeUser (user: any, baseURL: string): VssueAPI.User { + return { + username: user.login, + avatar: user.avatar_url, + homepage: concatURL(baseURL, user.login), + } +} + +export function normalizeIssue (issue: any, baseURL: string, owner: string, repo: string): VssueAPI.Issue { + return { + id: issue.number, + title: issue.title, + content: issue.body, + link: concatURL(baseURL, `${owner}/${repo}/issues/${issue.number}`), + } +} + +export function normalizeComment (comment: any, baseURL: string): VssueAPI.Comment { + return { + id: comment.id, + content: comment.body_html, + contentRaw: comment.body, + author: normalizeUser(comment.user, baseURL), + createdAt: comment.created_at, + updatedAt: comment.updated_at, + reactions: comment.reactions, + } +} + +export function normalizeReactions (reactions: any): VssueAPI.Reactions { + return { + like: reactions.filter(item => item.content === '+1').length, + unlike: reactions.filter(item => item.content === '-1').length, + heart: reactions.filter(item => item.content === 'heart').length, + } +} + +export function mapReactionName (reaction: keyof VssueAPI.Reactions): string { + if (reaction === 'like') return '+1' + if (reaction === 'unlike') return '-1' + return reaction +} + +export default { + normalizeUser, + normalizeIssue, + normalizeComment, +} diff --git a/packages/@vssue/api-gitea-v1/tsconfig.json b/packages/@vssue/api-gitea-v1/tsconfig.json new file mode 100644 index 00000000..6401b236 --- /dev/null +++ b/packages/@vssue/api-gitea-v1/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + }, + "include": [ + "./src" + ] + } From bdde5baa4f5cfaef88c0c234b438549d60c4e25a Mon Sep 17 00:00:00 2001 From: meteorlxy Date: Tue, 31 Dec 2019 16:17:42 +0800 Subject: [PATCH 3/4] feat(vssue): add gitea api v1 package --- packages/vssue/package.json | 1 + packages/vssue/scripts/rollup-config.js | 1 + packages/vssue/src/browser/vssue.gitea.ts | 8 ++++++++ packages/vssue/src/components/Iconfont.vue | 7 +++++++ 4 files changed, 17 insertions(+) create mode 100644 packages/vssue/src/browser/vssue.gitea.ts diff --git a/packages/vssue/package.json b/packages/vssue/package.json index a05f3217..e552f751 100644 --- a/packages/vssue/package.json +++ b/packages/vssue/package.json @@ -43,6 +43,7 @@ "@babel/core": "^7.4.5", "@babel/preset-env": "^7.4.5", "@vssue/api-bitbucket-v2": "^1.2.1", + "@vssue/api-gitea-v1": "^1.2.2", "@vssue/api-gitee-v5": "^1.2.1", "@vssue/api-github-v3": "^1.1.2", "@vssue/api-github-v4": "^1.2.1", diff --git a/packages/vssue/scripts/rollup-config.js b/packages/vssue/scripts/rollup-config.js index abe359dc..2e7ab1e4 100644 --- a/packages/vssue/scripts/rollup-config.js +++ b/packages/vssue/scripts/rollup-config.js @@ -16,6 +16,7 @@ const { const browserEntries = [ 'vssue.bitbucket', + 'vssue.gitea', 'vssue.gitee', 'vssue.github', 'vssue.github-v4', diff --git a/packages/vssue/src/browser/vssue.gitea.ts b/packages/vssue/src/browser/vssue.gitea.ts new file mode 100644 index 00000000..cb214de8 --- /dev/null +++ b/packages/vssue/src/browser/vssue.gitea.ts @@ -0,0 +1,8 @@ +import Vssue from '../main' +import GiteaV1 from '@vssue/api-gitea-v1' + +if (typeof window !== 'undefined' && (window).Vue) { + (window).Vue.use(Vssue, { + api: GiteaV1, + }) +} diff --git a/packages/vssue/src/components/Iconfont.vue b/packages/vssue/src/components/Iconfont.vue index 1a7b0fe6..5652f439 100644 --- a/packages/vssue/src/components/Iconfont.vue +++ b/packages/vssue/src/components/Iconfont.vue @@ -7,6 +7,13 @@ + + + + Date: Tue, 31 Dec 2019 16:17:49 +0800 Subject: [PATCH 4/4] docs: add gitea demo --- packages/docs/src/.vuepress/components/VssueDemo.vue | 11 +++++++++++ packages/docs/src/.vuepress/config.js | 1 + packages/docs/src/demo/gitea.md | 9 +++++++++ packages/docs/src/zh/demo/gitea.md | 9 +++++++++ 4 files changed, 30 insertions(+) create mode 100644 packages/docs/src/demo/gitea.md create mode 100644 packages/docs/src/zh/demo/gitea.md diff --git a/packages/docs/src/.vuepress/components/VssueDemo.vue b/packages/docs/src/.vuepress/components/VssueDemo.vue index b7965994..6f4dc6a9 100644 --- a/packages/docs/src/.vuepress/components/VssueDemo.vue +++ b/packages/docs/src/.vuepress/components/VssueDemo.vue @@ -7,6 +7,7 @@