Skip to content

Add custom headers support to Redash API calls #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Redash用Slack Bot V2

Redashbot V2は、[Redash](https://redash.io)のためのオープンソースSlack botです。

(hakobera/redashbotからフォークされましたが、ほぼすべてのコードを書き直し、V2として公開しています。)

## 機能

- Chartのスクリーンショット
- Dashboardのスクリーンショット
- 表形式の結果(※スクリーンショットではありません)
- Dockerデプロイメント
- <s>サーバーレスなデプロイメント</s>
- HTTP基盤の新しいSlackアプリ(非RTMスタイル)
- **オープンソース!**

![screenshot.png](./images/screenshot.png)

## 使用方法

- Visualization
- `@botname <Query URL>#<Viz ID>`
- e.g. `@redash https://your-redash-server.example.com/queries/1#2`
- Dashboard
- `@botname <Dashboard URL>`
- e.g. `@redash https://your-redash-server.example.com/dashboards/dashboard-name`
- Table
- `@botname <Query URL>#table`
- e.g. `@redash https://your-redash-server.example.com/queries/1#table`


## セットアップ

[Slackアプリを作成](https://api.slack.com/apps/)し、環境変数`SLACK_BOT_TOKEN`と`SLACK_SIGNING_SECRET`を設定してください。

[公式ドキュメント](https://slack.dev/bolt-js/tutorial/getting-started#create-an-app)を参照してください。

イベントサブスクリプションページでは、`Request URL`は`https://<your-domain>/slack/events`になります。

その後、`npm start`または`docker run yamitzky/redashbot:main`を実行して起動します。Dockerを使用する場合は、`-e`オプションまたは`.env`ファイルを介して環境変数を渡すことを忘れないでください。

### スラッシュコマンド(オプション)

`/redash-capture [URL]`でredashbotを使用できます。

アプリのスラッシュコマンドページで、[Create New Command]をクリックして送信します。`Command`は`/redash-capture`、`Request URL`は`https://<your-domain>/slack/events`を指定してください。

### ワークフローステップ(オプション)

redashbotをワークフローステップとして使用できます。

アプリのInteractivity & Shortcutsページで、Interactivityを有効にします。`Request URL`は`https://<your-domain>/slack/events`になります。

その後、Workflow Stepsページに移動し、[Add Step]をクリックして送信します。`Callback ID`は`redash_capture`を指定してください。

## 環境変数

### SLACK_BOT_TOKEN(必須)

SlackのBotトークン。

### SLACK_SIGNING_SECRET(必須)

Slackの署名シークレット。

### REDASH_HOSTとREDASH_API_KEY(オプション)

RedashのURLとそのAPIキー。

## REDASH_HOST_ALIAS(オプション)

Botからアクセス可能なRedashのURL。

### REDASH_HOSTS_AND_API_KEYS(オプション)

複数のRedashを一度に使用したい場合は、以下のようにこの変数を指定します。

```
REDASH_HOSTS_AND_API_KEYS="http://redash1.example.com;TOKEN1,http://redash2.example.com;TOKEN2"
```

または、各RedashにREDASH_HOST_ALIASを指定する必要がある場合は、以下のようにします。

```
REDASH_HOSTS_AND_API_KEYS="http://redash1.example.com;http://redash1-alias.example.com;TOKEN1,http://redash2.example.com;TOKEN2"
```

### SLEEP_TIME(オプション)

キャプチャ前に読み込み完了を待つミリ秒数。

### BROWSER(オプションかつ実験的)

`chromium`、`firefox`、または`webkit`。デフォルトは`chromium`です。

### REDASH_CUSTOM_HEADERS(オプション)

RedashリクエストにカスタムHTTPヘッダーを追加します。セミコロン区切りのkey:value形式で指定します。

例:
```
REDASH_CUSTOM_HEADERS="CF-Access-Client-Id:your-client-id;CF-Access-Client-Secret:your-client-secret"
```

## 開発方法

このリポジトリをクローンし、以下を実行します。

```bash
$ npm install
$ npx playwright install
$ export REDASH_HOST=https://your-redash-server.example.com
$ export REDASH_API_KEY=your-redash-api-key
$ export SLACK_BOT_TOKEN=your-slack-bot-token
$ npm start
```

## デプロイ

RedashbotはNodeプログラムとして作られています。

```
npm start
```

### Docker

[Dockerイメージ](https://hub.docker.com/r/yamitzky/redashbot)が提供されています。現在、`latest`タグはv1(旧バージョン)に使用されているため、`2.0.0`などを使用する必要があります。

```
docker run -it --rm -e SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN -e SLACK_SIGNING_SECRET=$SLACK_SIGNING_SECRET -e REDASH_HOSTS_AND_API_KEYS=$REDASH_HOSTS_AND_API_KEYS -p 3000:3000 yamitzky/redashbot:2.0.0
```

docker-composeも提供されています。

```
docker-compose up
```

### Heroku(テストされていません!)

以下のボタンをクリックするだけで、簡単にredashbotをHerokuにデプロイできます。

[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,26 @@ Milliseconds to wait loading finished before capturing.

`chromium`, `firefox` or `webkit`. default is `chromium`

### REDASH_CUSTOM_HEADERS (optional)

Add custom HTTP headers to Redash requests. Specify in a semicolon-separated key:value format.

Example:
```
REDASH_CUSTOM_HEADERS="CF-Access-Client-Id:your-client-id;CF-Access-Client-Secret:your-client-secret"
```

## How to develop

Clone this repository, then

```bash
$ npm install
$ npx playwright install
$ export REDASH_HOST=https://your-redash-server.example.com
$ export REDASH_API_KEY=your-redash-api-key
$ export SLACK_BOT_TOKEN=your-slack-bot-token
$ node index.js
$ npm start
```

## Deploy
Expand Down
6 changes: 3 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function createApp(config: Config & AppOptions) {

const browser = new Browser()
for (const [host, { alias, key: apiKey }] of Object.entries(config.hosts)) {
const redash = new Redash({ host, apiKey, alias })
const redash = new Redash({ host, apiKey, alias, headers: config.headers })
const ctx = { redash, browser }
for (const [path, handler] of handlers) {
app.message(new RegExp(`${host}${path}`), mention(), handler(ctx))
Expand All @@ -36,7 +36,7 @@ export function createApp(config: Config & AppOptions) {
await ack()

for (const [host, { alias, key: apiKey }] of Object.entries(config.hosts)) {
const redash = new Redash({ host, apiKey, alias })
const redash = new Redash({ host, apiKey, alias, headers: config.headers })
const ctx = { redash, browser }
for (const [path, handler] of handlers) {
const { command } = args
Expand Down Expand Up @@ -136,7 +136,7 @@ export function createApp(config: Config & AppOptions) {
for (const [host, { alias, key: apiKey }] of Object.entries(
config.hosts
)) {
const redash = new Redash({ host, apiKey, alias })
const redash = new Redash({ host, apiKey, alias, headers: config.headers })
const ctx = { redash, browser }
for (const [path, handler] of handlers) {
const matches = new RegExp(`${host}${path}`).exec(url)
Expand Down
6 changes: 3 additions & 3 deletions src/browser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sleep from 'await-sleep'
import playwright, { LaunchOptions } from 'playwright'
import * as lambdaPlaywright from 'playwright-aws-lambda'
import sleep from 'await-sleep'
import { config, Engine } from './config'
import { Engine, config } from './config'

async function launch(engine: Engine, options?: LaunchOptions) {
if (engine === 'lambda-chromium') {
Expand All @@ -27,7 +27,7 @@ export class Browser {
this.browser = await launch(config.browser, this.options)
}

const page = await this.browser.newPage()
const page = await this.browser.newPage({ extraHTTPHeaders: config.headers })
page.setViewportSize({ width: width, height: height })
await page.goto(url, { timeout: config.browserTimeout })
try {
Expand Down
27 changes: 21 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Config = {
browser: Engine
sleep: number
browserTimeout: number
headers?: Record<string, any>
}

let hosts: Hosts
Expand All @@ -33,17 +34,30 @@ if (process.env.REDASH_HOST) {
}
}
} else {
hosts = (process.env.REDASH_HOSTS_AND_API_KEYS || '')
.split(',')
.reduce((m, host_and_key) => {
hosts = (process.env.REDASH_HOSTS_AND_API_KEYS || '').split(',').reduce(
(m, host_and_key) => {
let [host, alias, key] = host_and_key.split(';')
if (!key) {
key = alias
alias = host
}
m[host] = { alias, key }
return m
}, {} as Record<string, { alias: string; key: string }>)
},
{} as Record<string, { alias: string; key: string }>,
)
}

const headers: Record<string, string> = {}
try {
if (process.env.REDASH_CUSTOM_HEADERS) {
for (const kv of process.env.REDASH_CUSTOM_HEADERS.split(';')) {
const [header, value] = kv.split(':', 2)
headers[header] = value
}
}
} catch (error) {
console.warn('Failed to parse REDASH_CUSTOM_HEADERS:', error)
}

export const config: Config = {
Expand All @@ -55,7 +69,8 @@ export const config: Config = {
browserTimeout: process.env.BROWSER_TIMEOUT
? parseFloat(process.env.BROWSER_TIMEOUT)
: process.env.SLEEP_TIME
? parseFloat(process.env.SLEEP_TIME)
: 10000,
? parseFloat(process.env.SLEEP_TIME)
: 10000,
hosts,
headers,
}
49 changes: 27 additions & 22 deletions src/redash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,56 +25,61 @@ export class Redash {
host: string
apiKey: string
alias: string
headers?: Record<string, string>

constructor({
host,
apiKey,
alias,
headers,
}: {
host: string
apiKey: string
alias: string
headers?: Record<string, string>
}) {
this.alias = alias
this.host = host
this.apiKey = apiKey
this.headers = headers
}

async getQuery(id: string): Promise<Query> {
const res = await axios.get(`${this.alias}/api/queries/${id}`, {
private getRequestConfig({
headers = {},
params = {},
}: { headers?: Record<string, any>; params?: Record<string, any> } = {}) {
return {
params: {
api_key: this.apiKey,
...params,
},
headers: {
...this.headers,
...headers,
},
})
}
}

async getQuery(id: string): Promise<Query> {
const res = await axios.get(`${this.alias}/api/queries/${id}`, this.getRequestConfig())
return res.data
}

async getQueryResult(id: string): Promise<QueryResult> {
const res = await axios.get(
`${this.alias}/api/queries/${id}/results.json`,
{
params: {
api_key: this.apiKey,
},
}
)
const res = await axios.get(`${this.alias}/api/queries/${id}/results.json`, this.getRequestConfig())
return res.data
}

async getDashboardLegacy(idOrSlug: string): Promise<Dashboard> {
const res = await axios.get(`${this.alias}/api/dashboards/${idOrSlug}`, {
params: {
api_key: this.apiKey,
legacy: true,
},
})
const res = await axios.get(
`${this.alias}/api/dashboards/${idOrSlug}`,
this.getRequestConfig({ params: { legacy: true } }),
)
return res.data
}

async getDashboard(id: string): Promise<Dashboard> {
const res = await axios.get(`${this.alias}/api/dashboards/${id}`, {
params: {
api_key: this.apiKey,
},
})
const res = await axios.get(`${this.alias}/api/dashboards/${id}`, this.getRequestConfig())
return res.data
}
}