Coder Social home page Coder Social logo

koishi-plugin-github's Introduction

Codecov npm

GitHub Toolkit for Koishi.

文档

https://github.koishi.chat

许可证

使用 MIT 许可证发布。

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

koishi-plugin-github's People

Contributors

catlair avatar cn-traveler avatar darwintree avatar microcber avatar shigma avatar xxlittlecxx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

koishi-plugin-github's Issues

Bug: 使用 github 插件的快速回复功能,会导致覆盖 PR 中的文件点评的楼主发言

Describe the bug

RT
详细的复现步骤见下方说明及截图

Steps to reproduce

  1. 在任意群内使用 github.repos 指令监听一个 GitHub 仓库
  2. 在目标 repo 中发起一个新的 PR,此时 bot 会广播本次 PR
  3. 开始一个 review,在任意一个文件的任意一行添加行内评论,例如“AAAAA”,发布 review
  4. bot 首先广播本次 review 的主体内容,然后广播行内评论
  5. 回复 bot 广播的行内回复,例如“BBBBB”
  6. 触发 bug:原本题主在行内留下的评论“AAAAA”被后来使用 bot 回复的用户直接覆盖为了“BBBBB”

Expected behavior

应新增回复,而不是覆盖题主的评论

Screenshots

图片
图片
图片
图片
图片

Versions

  • OS:
  • Platform:
  • Node version:
  • Koishi version:

Additional context

No response

Github Action 支持

有些人 Github 访问困难 看个 action 十分不方便

希望可以支持 Action 在 报错 / 成功 后有 bot 针对该事件的响应

Bug: render error for markdown

> Well, I have learnt more...........own go-cqhttp without the adorable Koishi.

I actually think one of us (probably it's me) doesn't completely understand the source code. Let's take a look at this part.

\`\`\`go
	if len(byteKey) == 0 {
		log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
		cancel := make(chan struct{}, 1)
		state, _ := term.GetState(int(os.Stdin.Fd()))
		go func() {
			select {
			case <-cancel:
				return
			case <-time.After(time.Second * 45):
				log.Infof("解密key输入超时")
				time.Sleep(3 * time.Second)
				_ = term.Restore(int(os.Stdin.Fd()), state)
				os.Exit(0)
			}
		}()
		byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
		cancel <- struct{}{}
	} else {
		log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
	}

	encrypt, _ := os.ReadFile("password.encrypt")
	ph, err := PasswordHashDecrypt(string(encrypt), byteKey)
	if err != nil {
		log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
	}
	copy(base.PasswordHash[:], ph)
\`\`\`

In my understanding, it would first check if there's an argument that provides the 'password' to decrypt the login password. If so, use it to decrypt the password, else wise, ask the user to input the password in the terminal. This would solve the problem that Masnn mentioned: the 'password' to decrypt the login password is not stored and is memorized by the admin, so only if the user inputs the password in the terminal or provide it through arguments, gocqhttp would be able to decrypt the password. Elsewise, neither gocqhttp nor other software would be able to decrypt it.

-----------

Also, I'd like to share some situations when we need it:
1. It would be helpful if Koishi backend has some critical vulnerability for arbitrary file reading.
2. I've seen many users directly send their `config.yml` out when they are asking for help. This would prevent accidental password leaks.
3. When the server was cracked, this would stop the hacker from stealing the account. Ofc this won't work if the hacker get permission of reading memory, but it still works when the hacker cannot get root permission.
4. It protects the user who doesn't use `auth` plugin.

---------------

The workflow can be easily simplified with the password args. We just need to (fake code):
\`\`\`js
pwd_encrypt = ask the user for the password.
if( file "password.encrypt" not exists ){
    pwd = ask the user for qq password
    write pwd to config.yml and enable encrypt option
    start process 'gocqhttp'
    pass the password to standard input
    wait until it exits
}
remove the password in config.yml, save it.
start process 'gocqhttp' with arg `--pwd=${pwd_encrypt}`
\`\`\`

---------

Anyway, I think that's a good feature and I'd like to leave the issue here. You contributors take the final determination if implement this feature or not.

----------

![(5@ DXGO16ACC_EC{MO2FSN](https://user-images.githubusercontent.com/66859419/215329111-69757232-2161-4dca-968a-feb863c6f469.jpeg)


Sincerely,
MicroBlock

this markdown string would cause a render error in the message like this:

image

dont know if this is intentional or not

无法授权登录 max listener count (64) for event <ready> exceeded, which may be caused by a memory leak

log摘要:

[app] max listener count (64) for event exceeded, which may be caused by a memory leak

进行的操作

image

声明

已多次确认appid与appsecert正确

环境

System:
OS: Linux 5.4 Alpine Linux
CPU: (8) x64 Intel(R) Xeon(R) Gold 6133 CPU @ 2.50GHz

Binaries:
Node: 18.12.1
Yarn: 1.22.19

Koishi:
Core: 4.10.10
Console: 5.0.0
平台:沙盒

操作 log 数据库展示视频

https://musetransfer.com/s/py3ie6tau(有效期至2023年1月4日)|【Muse】你有一份文件待查收,请点击链接获取文件

让我试试机器人授权链接后的行为

点击链接加入群聊【zpi、多玩幻灵qwq】:https://jq.qq.com/?_wv=1027&k=n3UflT2L

feat: subscribe to repo without webhook permission

目前,本插件实现的subscribe功能基于webhook,因此需要得到webhook的写入(与删除)权限。

但有时我们可能需要订阅部分开源项目的仓库,此时bot是无法拿到webhook的写入权限的。

不知道是否有计划支持相关feature?

Bug: edit-comment-event shows the original author instead of operator currently

Summary

GitHub allows administrative users to change other users' posts, but the message that the bot pushed only shows the original author instead of the operator in the current event.

Exemples

This comment was posted by poor @MaikoTan or me, but got edited several times by @SaarChaffee , every time they change the content of the comment, the bot would say something like "@MaikoTan edited the comment in blahblah" instead of @SaarChaffee who is the one changed things.

image

登录授权后 github返回404

Screenshot_2023-11-25-21-46-04-138_com.tencent.wework.png

Screenshot_2023-11-25-21-44-35-014_com.tencent.wework.jpg

还有一个问题 在TX使用的时候 github链接无法发出 能否增加前缀地址(在TX报备过的)做一下跳转

Bug: broadcast step is failed

Describe Bug

发现在 #5 中有提到失效的问题,目前我似乎也遇到这个问题。我目前定位到的有可能是 broadcast 的问题。

image

image

GitHub 成功发送了信息,并且在 ctx.broadcast 前似乎都是正常的,但在 ctx.broadcast 后却什么都没有返回,信息也没有发出去。

image

中间发生了什么错误我没去继续查

Addition Context

Bot 的发言没有受到 QQ 限制

Bug: Github oauth未正常工作

Describe the bug

Github oauth未正常工作,无法正确的绑定到用户

Steps to reproduce

如下面的截图所配置,然后按照正常的流程走即可出现

Expected behavior

成功绑定用户

Screenshots

image
image
image

Versions

  • OS: CentOS 7.9.2009 x86_64
  • Platform: onebot
  • Node version: v14.19.0
  • Koishi version: 4.6.1

Additional context

1651027016375.mp4

Feature: Require "reply" or "r" prefix when replying

As the title suggests. When users precisely want to reply to an issue. Users will never mind adding words when they need this feature. It also prevents users from misusing and reduces the bot's message sending.

Bug: 无法发送消息

按文档配置了插件,群聊中已订阅仓库。github webhook 发送请求后能接收到 response,但不会往群聊中发送消息。能正常使用插件新建 issue。

c1a50c2e5367fe51686d9a4ccd49a933

环境信息

System:
    OS: Linux 5.4 Alpine Linux
    CPU: (2) x64 Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz

Binaries:
    Node: 20.9.0
    Yarn: 4.0.1

Koishi:
    Core: 4.17.1
    Console: 5.26.0

github webhook payload

{
  "action": "created",
  "issue": {
    "url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/9",
    "repository_url": "https://api.github.com/repos/null-qwerty/null-qwerty",
    "labels_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/9/labels{/name}",
    "comments_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/9/comments",
    "events_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/9/events",
    "html_url": "https://github.com/null-qwerty/null-qwerty/issues/9",
    "id": 2152638646,
    "node_id": "I_kwDOK4YM4c6ATqi2",
    "number": 9,
    "title": "5",
    "user": {
      "login": "null-qwerty",
      "id": 114760660,
      "node_id": "U_kgDOBtcb1A",
      "avatar_url": "https://avatars.githubusercontent.com/u/114760660?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/null-qwerty",
      "html_url": "https://github.com/null-qwerty",
      "followers_url": "https://api.github.com/users/null-qwerty/followers",
      "following_url": "https://api.github.com/users/null-qwerty/following{/other_user}",
      "gists_url": "https://api.github.com/users/null-qwerty/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/null-qwerty/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/null-qwerty/subscriptions",
      "organizations_url": "https://api.github.com/users/null-qwerty/orgs",
      "repos_url": "https://api.github.com/users/null-qwerty/repos",
      "events_url": "https://api.github.com/users/null-qwerty/events{/privacy}",
      "received_events_url": "https://api.github.com/users/null-qwerty/received_events",
      "type": "User",
      "site_admin": false
    },
    "labels": [

    ],
    "state": "open",
    "locked": false,
    "assignee": null,
    "assignees": [

    ],
    "milestone": null,
    "comments": 5,
    "created_at": "2024-02-25T07:24:05Z",
    "updated_at": "2024-03-02T06:47:32Z",
    "closed_at": null,
    "author_association": "OWNER",
    "active_lock_reason": null,
    "body": "5",
    "reactions": {
      "url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/9/reactions",
      "total_count": 0,
      "+1": 0,
      "-1": 0,
      "laugh": 0,
      "hooray": 0,
      "confused": 0,
      "heart": 0,
      "rocket": 0,
      "eyes": 0
    },
    "timeline_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/9/timeline",
    "performed_via_github_app": null,
    "state_reason": "reopened"
  },
  "comment": {
    "url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/comments/1974462858",
    "html_url": "https://github.com/null-qwerty/null-qwerty/issues/9#issuecomment-1974462858",
    "issue_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/9",
    "id": 1974462858,
    "node_id": "IC_kwDOK4YM4c51r-mK",
    "user": {
      "login": "null-qwerty",
      "id": 114760660,
      "node_id": "U_kgDOBtcb1A",
      "avatar_url": "https://avatars.githubusercontent.com/u/114760660?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/null-qwerty",
      "html_url": "https://github.com/null-qwerty",
      "followers_url": "https://api.github.com/users/null-qwerty/followers",
      "following_url": "https://api.github.com/users/null-qwerty/following{/other_user}",
      "gists_url": "https://api.github.com/users/null-qwerty/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/null-qwerty/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/null-qwerty/subscriptions",
      "organizations_url": "https://api.github.com/users/null-qwerty/orgs",
      "repos_url": "https://api.github.com/users/null-qwerty/repos",
      "events_url": "https://api.github.com/users/null-qwerty/events{/privacy}",
      "received_events_url": "https://api.github.com/users/null-qwerty/received_events",
      "type": "User",
      "site_admin": false
    },
    "created_at": "2024-03-02T06:47:31Z",
    "updated_at": "2024-03-02T06:47:31Z",
    "author_association": "OWNER",
    "body": "123456",
    "reactions": {
      "url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/comments/1974462858/reactions",
      "total_count": 0,
      "+1": 0,
      "-1": 0,
      "laugh": 0,
      "hooray": 0,
      "confused": 0,
      "heart": 0,
      "rocket": 0,
      "eyes": 0
    },
    "performed_via_github_app": null
  },
  "repository": {
    "id": 730205409,
    "node_id": "R_kgDOK4YM4Q",
    "name": "null-qwerty",
    "full_name": "null-qwerty/null-qwerty",
    "private": false,
    "owner": {
      "login": "null-qwerty",
      "id": 114760660,
      "node_id": "U_kgDOBtcb1A",
      "avatar_url": "https://avatars.githubusercontent.com/u/114760660?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/null-qwerty",
      "html_url": "https://github.com/null-qwerty",
      "followers_url": "https://api.github.com/users/null-qwerty/followers",
      "following_url": "https://api.github.com/users/null-qwerty/following{/other_user}",
      "gists_url": "https://api.github.com/users/null-qwerty/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/null-qwerty/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/null-qwerty/subscriptions",
      "organizations_url": "https://api.github.com/users/null-qwerty/orgs",
      "repos_url": "https://api.github.com/users/null-qwerty/repos",
      "events_url": "https://api.github.com/users/null-qwerty/events{/privacy}",
      "received_events_url": "https://api.github.com/users/null-qwerty/received_events",
      "type": "User",
      "site_admin": false
    },
    "html_url": "https://github.com/null-qwerty/null-qwerty",
    "description": null,
    "fork": false,
    "url": "https://api.github.com/repos/null-qwerty/null-qwerty",
    "forks_url": "https://api.github.com/repos/null-qwerty/null-qwerty/forks",
    "keys_url": "https://api.github.com/repos/null-qwerty/null-qwerty/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/null-qwerty/null-qwerty/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/null-qwerty/null-qwerty/teams",
    "hooks_url": "https://api.github.com/repos/null-qwerty/null-qwerty/hooks",
    "issue_events_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/events{/number}",
    "events_url": "https://api.github.com/repos/null-qwerty/null-qwerty/events",
    "assignees_url": "https://api.github.com/repos/null-qwerty/null-qwerty/assignees{/user}",
    "branches_url": "https://api.github.com/repos/null-qwerty/null-qwerty/branches{/branch}",
    "tags_url": "https://api.github.com/repos/null-qwerty/null-qwerty/tags",
    "blobs_url": "https://api.github.com/repos/null-qwerty/null-qwerty/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/null-qwerty/null-qwerty/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/null-qwerty/null-qwerty/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/null-qwerty/null-qwerty/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/null-qwerty/null-qwerty/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/null-qwerty/null-qwerty/languages",
    "stargazers_url": "https://api.github.com/repos/null-qwerty/null-qwerty/stargazers",
    "contributors_url": "https://api.github.com/repos/null-qwerty/null-qwerty/contributors",
    "subscribers_url": "https://api.github.com/repos/null-qwerty/null-qwerty/subscribers",
    "subscription_url": "https://api.github.com/repos/null-qwerty/null-qwerty/subscription",
    "commits_url": "https://api.github.com/repos/null-qwerty/null-qwerty/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/null-qwerty/null-qwerty/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/null-qwerty/null-qwerty/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/null-qwerty/null-qwerty/contents/{+path}",
    "compare_url": "https://api.github.com/repos/null-qwerty/null-qwerty/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/null-qwerty/null-qwerty/merges",
    "archive_url": "https://api.github.com/repos/null-qwerty/null-qwerty/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/null-qwerty/null-qwerty/downloads",
    "issues_url": "https://api.github.com/repos/null-qwerty/null-qwerty/issues{/number}",
    "pulls_url": "https://api.github.com/repos/null-qwerty/null-qwerty/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/null-qwerty/null-qwerty/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/null-qwerty/null-qwerty/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/null-qwerty/null-qwerty/labels{/name}",
    "releases_url": "https://api.github.com/repos/null-qwerty/null-qwerty/releases{/id}",
    "deployments_url": "https://api.github.com/repos/null-qwerty/null-qwerty/deployments",
    "created_at": "2023-12-11T12:31:23Z",
    "updated_at": "2023-12-11T12:31:24Z",
    "pushed_at": "2024-03-02T06:08:24Z",
    "git_url": "git://github.com/null-qwerty/null-qwerty.git",
    "ssh_url": "[email protected]:null-qwerty/null-qwerty.git",
    "clone_url": "https://github.com/null-qwerty/null-qwerty.git",
    "svn_url": "https://github.com/null-qwerty/null-qwerty",
    "homepage": null,
    "size": 30,
    "stargazers_count": 0,
    "watchers_count": 0,
    "language": null,
    "has_issues": true,
    "has_projects": true,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": false,
    "has_discussions": false,
    "forks_count": 0,
    "mirror_url": null,
    "archived": false,
    "disabled": false,
    "open_issues_count": 1,
    "license": null,
    "allow_forking": true,
    "is_template": false,
    "web_commit_signoff_required": false,
    "topics": [

    ],
    "visibility": "public",
    "forks": 0,
    "open_issues": 1,
    "watchers": 0,
    "default_branch": "main"
  },
  "sender": {
    "login": "null-qwerty",
    "id": 114760660,
    "node_id": "U_kgDOBtcb1A",
    "avatar_url": "https://avatars.githubusercontent.com/u/114760660?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/null-qwerty",
    "html_url": "https://github.com/null-qwerty",
    "followers_url": "https://api.github.com/users/null-qwerty/followers",
    "following_url": "https://api.github.com/users/null-qwerty/following{/other_user}",
    "gists_url": "https://api.github.com/users/null-qwerty/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/null-qwerty/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/null-qwerty/subscriptions",
    "organizations_url": "https://api.github.com/users/null-qwerty/orgs",
    "repos_url": "https://api.github.com/users/null-qwerty/repos",
    "events_url": "https://api.github.com/users/null-qwerty/events{/privacy}",
    "received_events_url": "https://api.github.com/users/null-qwerty/received_events",
    "type": "User",
    "site_admin": false
  }
}

Feat: query issues' content

Enable user to query a certain issue by repo name and issue id, e.g. gh.issue --repo koishijs/koishi #233

Bug: 无法使用部分重要功能

仓库部分功能失效

2024-04-28 00:51:37 [W] github Error: Unprocessable Entity
                            at [cordis.invoke] (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\@cordisjs\plugin-http\lib\index.cjs:331:23)
                            at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
                            at async Proxy.request (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\koishi-plugin-github\lib\index.js:77:20)
                            at async Command.<anonymous> (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\koishi-plugin-github\lib\command.js:68:28)
                            at async Array.<anonymous> (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\@koishijs\core\lib\index.cjs:1149:14)
                            at async Command.execute (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\@koishijs\core\lib\index.cjs:1163:22)
                            at async C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\@koishijs\core\lib\index.cjs:2182:22
                            at async Proxy.withScope (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\@koishijs\core\lib\index.cjs:2085:22)
                            at async next (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\@koishijs\core\lib\index.cjs:888:16)
                            at async next (C:\Users\Administrator\AppData\Roaming\Koishi\Desktop\data\instances\default\node_modules\@koishijs\core\lib\index.cjs:888:16)

image

定义的Path未生效

版本5.6.1
访问server定义的selfUrl/path/authorize
被重定向至selfUrl(控制台)
预期内请求selfUrl/path/authorize标题不应该为Koishi 控制台

Bug: Webhook 功能失效

  • github webhook 显示成功发送(200)
  • 无报错,无消息发出

目前尚不明确原因。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.