@book000/node-utils 1.2.28 → 1.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintignore ADDED
@@ -0,0 +1,5 @@
1
+ dist
2
+ output
3
+ node_modules
4
+ data
5
+ logs
package/.eslintrc.yml ADDED
@@ -0,0 +1,21 @@
1
+ env:
2
+ es2020: true
3
+ node: true
4
+ extends:
5
+ - standard
6
+ - plugin:@typescript-eslint/recommended
7
+ - plugin:unicorn/recommended
8
+ - prettier
9
+ parser: '@typescript-eslint/parser'
10
+ parserOptions:
11
+ ecmaVersion: latest
12
+ sourceType: module
13
+ project: ./tsconfig.json
14
+ plugins:
15
+ - '@typescript-eslint'
16
+ rules:
17
+ '@typescript-eslint/no-explicit-any': off
18
+ '@typescript-eslint/ban-ts-comment': off
19
+ 'unicorn/prefer-top-level-await': off
20
+ 'unicorn/no-null': off
21
+ 'no-console': error
@@ -0,0 +1 @@
1
+ github: book000
@@ -0,0 +1,18 @@
1
+ # Node.js でビルド・テストを実行する。バージョンは .node-version に記載されているものを利用する
2
+
3
+ name: Node CI
4
+
5
+ on:
6
+ push:
7
+ branches:
8
+ - main
9
+ - master
10
+ pull_request:
11
+ branches:
12
+ - main
13
+ - master
14
+
15
+ jobs:
16
+ node-ci:
17
+ name: Node CI
18
+ uses: book000/templates/.github/workflows/reusable-nodejs-ci.yml@master
@@ -0,0 +1,63 @@
1
+ name: Release
2
+
3
+ on:
4
+ pull_request_target:
5
+ branches:
6
+ - main
7
+ - master
8
+ types:
9
+ - closed
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}
13
+
14
+ jobs:
15
+ release:
16
+ name: Release
17
+ runs-on: ubuntu-latest
18
+ if: github.event.pull_request.merged == true
19
+
20
+ steps:
21
+ - name: 🛎 Checkout
22
+ uses: actions/checkout@v3
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: 🏗 Setup node
27
+ uses: actions/setup-node@v3
28
+ with:
29
+ node-version-file: .node-version
30
+ cache: yarn
31
+ cache-dependency-path: yarn.lock
32
+ registry-url: 'https://registry.npmjs.org'
33
+
34
+ - name: 👨🏻‍💻 Install dependencies
35
+ run: yarn install --frozen-lockfile
36
+
37
+ - name: 🏃 Build
38
+ run: yarn build
39
+
40
+ - name: 🏷 Bump version and push tag
41
+ id: tag-version
42
+ uses: mathieudutour/github-tag-action@v6.1
43
+ with:
44
+ github_token: ${{ secrets.GITHUB_TOKEN }}
45
+ default_bump: 'minor'
46
+ custom_release_rules: 'feat:minor:✨ Features,fix:patch:🐛 Fixes,docs:patch:📰 Docs,chore:patch:🎨 Chore,pref:patch:🎈 Performance improvements,refactor:patch:🧹 Refactoring,build:patch:🔍 Build,ci:patch:🔍 CI,revert:patch:⏪ Revert,style:patch:🧹 Style,test:patch:👀 Test,release:major:📦 Release'
47
+
48
+ - name: 📦 Publish
49
+ run: yarn publish --access public --non-interactive --no-git-tag-version --no-git-reset --no-commit-hooks --new-version ${{ steps.tag-version.outputs.new_version }}
50
+ env:
51
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
52
+
53
+ - name: 📃 Create Release
54
+ id: create_release
55
+ uses: actions/create-release@v1
56
+ env:
57
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58
+ with:
59
+ tag_name: ${{ steps.tag-version.outputs.new_tag }}
60
+ release_name: ${{ steps.tag-version.outputs.new_tag }}
61
+ body: ${{ steps.tag-version.outputs.changelog }}
62
+ draft: false
63
+ prerelease: false
package/.gitignore ADDED
@@ -0,0 +1,130 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+ .pnpm-debug.log*
9
+
10
+ # Diagnostic reports (https://nodejs.org/api/report.html)
11
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12
+
13
+ # Runtime data
14
+ pids
15
+ *.pid
16
+ *.seed
17
+ *.pid.lock
18
+
19
+ # Directory for instrumented libs generated by jscoverage/JSCover
20
+ lib-cov
21
+
22
+ # Coverage directory used by tools like istanbul
23
+ coverage
24
+ *.lcov
25
+
26
+ # nyc test coverage
27
+ .nyc_output
28
+
29
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30
+ .grunt
31
+
32
+ # Bower dependency directory (https://bower.io/)
33
+ bower_components
34
+
35
+ # node-waf configuration
36
+ .lock-wscript
37
+
38
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
39
+ build/Release
40
+
41
+ # Dependency directories
42
+ node_modules/
43
+ jspm_packages/
44
+
45
+ # Snowpack dependency directory (https://snowpack.dev/)
46
+ web_modules/
47
+
48
+ # TypeScript cache
49
+ *.tsbuildinfo
50
+
51
+ # Optional npm cache directory
52
+ .npm
53
+
54
+ # Optional eslint cache
55
+ .eslintcache
56
+
57
+ # Optional stylelint cache
58
+ .stylelintcache
59
+
60
+ # Microbundle cache
61
+ .rpt2_cache/
62
+ .rts2_cache_cjs/
63
+ .rts2_cache_es/
64
+ .rts2_cache_umd/
65
+
66
+ # Optional REPL history
67
+ .node_repl_history
68
+
69
+ # Output of 'npm pack'
70
+ *.tgz
71
+
72
+ # Yarn Integrity file
73
+ .yarn-integrity
74
+
75
+ # dotenv environment variable files
76
+ .env
77
+ .env.development.local
78
+ .env.test.local
79
+ .env.production.local
80
+ .env.local
81
+
82
+ # parcel-bundler cache (https://parceljs.org/)
83
+ .cache
84
+ .parcel-cache
85
+
86
+ # Next.js build output
87
+ .next
88
+ out
89
+
90
+ # Nuxt.js build / generate output
91
+ .nuxt
92
+ dist
93
+
94
+ # Gatsby files
95
+ .cache/
96
+ # Comment in the public line in if your project uses Gatsby and not Next.js
97
+ # https://nextjs.org/blog/next-9-1#public-directory-support
98
+ # public
99
+
100
+ # vuepress build output
101
+ .vuepress/dist
102
+
103
+ # vuepress v2.x temp and cache directory
104
+ .temp
105
+ .cache
106
+
107
+ # Docusaurus cache and generated files
108
+ .docusaurus
109
+
110
+ # Serverless directories
111
+ .serverless/
112
+
113
+ # FuseBox cache
114
+ .fusebox/
115
+
116
+ # DynamoDB Local files
117
+ .dynamodb/
118
+
119
+ # TernJS port file
120
+ .tern-port
121
+
122
+ # Stores VSCode versions used for testing VSCode extensions
123
+ .vscode-test
124
+
125
+ # yarn v2
126
+ .yarn/cache
127
+ .yarn/unplugged
128
+ .yarn/build-state.yml
129
+ .yarn/install-state.gz
130
+ .pnp.*
package/.node-version ADDED
@@ -0,0 +1 @@
1
+ 18.16.0
@@ -0,0 +1,12 @@
1
+ printWidth: 80
2
+ tabWidth: 2
3
+ useTabs: false
4
+ semi: false
5
+ quoteProps: 'as-needed'
6
+ singleQuote: true
7
+ jsxSingleQuote: true
8
+ trailingComma: 'es5'
9
+ bracketSpacing: true
10
+ bracketSameLine: true
11
+ arrowParens: 'always'
12
+ endOfLine: lf
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@book000/node-utils",
3
- "version": "1.2.28",
3
+ "version": "1.2.29",
4
4
  "description": "Self-Utility library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
- "dist/"
8
+ "dist/",
9
+ "!dist/examples/"
9
10
  ],
10
11
  "repository": "git@github.com:book000/node-utils.git",
11
12
  "author": "Tomachi <tomachi@tomacheese.com>",
package/renovate.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": [
3
+ "config:base"
4
+ ],
5
+ "ignorePresets": [
6
+ ":prHourlyLimit2"
7
+ ],
8
+ "timezone": "Asia/Tokyo",
9
+ "dependencyDashboard": false,
10
+ "automerge": true,
11
+ "branchConcurrentLimit": 0
12
+ }
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs'
2
+ import { parse } from 'jsonc-parser'
3
+
4
+ /**
5
+ * 設定ファイルを管理するフレームワーククラス
6
+ *
7
+ * 設定ファイルは JSONC 形式でパースされる。
8
+ *
9
+ * @template IConfig 設定ファイルの型
10
+ */
11
+ export abstract class ConfigFramework<IConfig> {
12
+ /** 設定ファイルのパス */
13
+ private path: string
14
+ private config: IConfig | undefined
15
+ private validateFailures: string[] = []
16
+
17
+ /**
18
+ * コンストラクタ
19
+ *
20
+ * ファイルの読み込みは行わない。{@link load} を呼び出すことで読み込む。
21
+ *
22
+ * 原則、以下の環境変数を利用する。値の場所にファイルがあればそれを利用する。
23
+ * - CONFIG_PATH
24
+ * - CONFIG_FILE
25
+ * 環境変数に値が設定されておらず、設定ファイルのパスが path に指定されていてそのファイルがある場合はそのファイルを設定ファイルとして使用する
26
+ * いずれの方法でもパスを取得できない場合はエラーを投げる
27
+ *
28
+ * @param path 設定ファイルのパス
29
+ * @returns インスタンス
30
+ */
31
+ constructor(path?: string) {
32
+ const paths = [
33
+ process.env.CONFIG_PATH,
34
+ process.env.CONFIG_FILE,
35
+ path,
36
+ ].filter((p) => p !== undefined && fs.existsSync(p)) as string[]
37
+ if (paths.length === 0) {
38
+ throw new Error('Config path not found')
39
+ }
40
+ this.path = paths[0]
41
+ }
42
+
43
+ /**
44
+ * 設定ファイルの検証ルールを定義する
45
+ *
46
+ * key には検証ルールの名前を指定する。
47
+ * value には検証ルールを定義する関数を指定する。
48
+ *
49
+ * @returns 検証ルール
50
+ */
51
+ protected abstract validates(): {
52
+ [key: string]: (config: IConfig) => boolean
53
+ }
54
+
55
+ /**
56
+ * 設定ファイルを読み込む
57
+ *
58
+ * 設定ファイルのパスはコンストラクタで指定されたものが利用される。
59
+ * この関数内ではバリデーションは行わないので、{@link validate} を呼び出す必要がある。
60
+ */
61
+ public load(): void {
62
+ const data = fs.readFileSync(this.path, 'utf8')
63
+ const json = parse(data) as IConfig
64
+ this.config = json
65
+ }
66
+
67
+ /**
68
+ * 設定ファイルのバリデーションを行う
69
+ *
70
+ * この関数を呼び出す前に、{@link load} を呼び出して設定ファイルを読み込んでおく必要がある。
71
+ * バリデーションに失敗した場合は、{@link getValidateFailures} で失敗した項目を取得できる。
72
+ *
73
+ * @returns バリデーションに成功した場合は true、失敗した場合は false
74
+ */
75
+ public validate(): boolean {
76
+ if (!this.config) throw new Error('Config not loaded')
77
+
78
+ this.validateFailures = []
79
+ const validates = this.validates()
80
+ for (const key in validates) {
81
+ try {
82
+ if (!validates[key](this.config)) {
83
+ this.validateFailures.push(key)
84
+ }
85
+ } catch {
86
+ this.validateFailures.push(key)
87
+ }
88
+ }
89
+ return this.validateFailures.length === 0
90
+ }
91
+
92
+ /**
93
+ * バリデーションに失敗した項目を取得する
94
+ *
95
+ * @returns バリデーションに失敗した項目の配列
96
+ */
97
+ public getValidateFailures(): string[] {
98
+ return this.validateFailures
99
+ }
100
+
101
+ /**
102
+ * 設定の値を取得する
103
+ *
104
+ * @param key 設定のキー
105
+ * @returns 設定の値
106
+ */
107
+ public get<T extends keyof IConfig>(key: T): IConfig[T] {
108
+ if (!this.config) throw new Error('Config not loaded')
109
+ return this.config[key]
110
+ }
111
+ }
package/src/cycle.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module 'cycle' {
2
+ export function decycle(object: any): any
3
+ export function retrocycle(json: any): any
4
+ }
package/src/discord.ts ADDED
@@ -0,0 +1,219 @@
1
+ import axios from 'axios'
2
+ import FormData from 'form-data'
3
+
4
+ interface DiscordBotOptions {
5
+ token: string
6
+ channelId: string
7
+ }
8
+
9
+ interface DiscordWebhookOptions {
10
+ webhookUrl: string
11
+ }
12
+
13
+ export type DiscordOptions = DiscordBotOptions | DiscordWebhookOptions
14
+ export interface DiscordEmbedFooter {
15
+ text: string
16
+ icon_url?: string
17
+ proxy_icon_url?: string
18
+ }
19
+
20
+ export interface DiscordEmbedImage {
21
+ url?: string
22
+ proxy_url?: string
23
+ height?: number
24
+ width?: number
25
+ }
26
+
27
+ export interface DiscordEmbedThumbnail {
28
+ url?: string
29
+ proxy_url?: string
30
+ height?: number
31
+ width?: number
32
+ }
33
+
34
+ export interface DiscordEmbedVideo {
35
+ url?: string
36
+ proxy_url?: string
37
+ height?: number
38
+ width?: number
39
+ }
40
+
41
+ export interface DiscordEmbedProvider {
42
+ name?: string
43
+ url?: string
44
+ }
45
+
46
+ export interface DiscordEmbedAuthor {
47
+ name?: string
48
+ url?: string
49
+ icon_url?: string
50
+ proxy_icon_url?: string
51
+ }
52
+
53
+ export interface DiscordEmbedField {
54
+ name: string
55
+ value: string
56
+ inline?: boolean
57
+ }
58
+
59
+ export interface DiscordEmbed {
60
+ title?: string
61
+ type?: 'rich' | 'image' | 'video' | 'gifv' | 'article' | 'link'
62
+ description?: string
63
+ url?: string
64
+ timestamp?: string
65
+ color?: number
66
+ footer?: DiscordEmbedFooter
67
+ image?: DiscordEmbedImage
68
+ thumbnail?: DiscordEmbedThumbnail
69
+ video?: DiscordEmbedVideo
70
+ provider?: DiscordEmbedProvider
71
+ author?: DiscordEmbedAuthor
72
+ fields?: DiscordEmbedField[]
73
+ }
74
+
75
+ interface DiscordNormalMessage {
76
+ content: string
77
+ }
78
+
79
+ interface DiscordEmbedMessage {
80
+ embeds: DiscordEmbed[]
81
+ }
82
+
83
+ interface DiscordFile {
84
+ name: string
85
+ file: ArrayBuffer
86
+ contentType?: string
87
+ isSpoiler?: boolean
88
+ }
89
+
90
+ interface DiscordFileMessage {
91
+ file: DiscordFile
92
+ }
93
+
94
+ export type DiscordMessage =
95
+ | DiscordNormalMessage
96
+ | DiscordEmbedMessage
97
+ | DiscordFileMessage
98
+
99
+ export class Discord {
100
+ private options: DiscordOptions
101
+
102
+ constructor(options: DiscordOptions) {
103
+ // token があれば Bot として動作する
104
+ // webhookUrl と channelId があれば Webhook として動作する
105
+ // どちらもなければエラーを投げる
106
+
107
+ if (this.isDiscordBotOptions(options)) {
108
+ this.options = options
109
+ } else if (this.isDiscordWebhookOptions(options)) {
110
+ this.options = options
111
+ } else {
112
+ throw new Error('Invalid options')
113
+ }
114
+ }
115
+
116
+ public static get validations(): {
117
+ [key: string]: (options: any) => boolean
118
+ } {
119
+ return {
120
+ 'token or webhookUrl and channelId': (options: any) =>
121
+ 'token' in options ||
122
+ ('webhookUrl' in options && 'channelId' in options),
123
+ 'token is valid': (options: any) => typeof options.token === 'string',
124
+ 'webhookUrl is valid': (options: any) =>
125
+ typeof options.webhookUrl === 'string',
126
+ 'channelId is valid': (options: any) =>
127
+ typeof options.channelId === 'string',
128
+ }
129
+ }
130
+
131
+ public async sendMessage(message: string | DiscordMessage): Promise<void> {
132
+ const formData = new FormData()
133
+
134
+ if (typeof message === 'string') {
135
+ formData.append('payload_json', JSON.stringify({ content: message }))
136
+ } else {
137
+ formData.append(
138
+ 'payload_json',
139
+ JSON.stringify({
140
+ content: 'content' in message ? message.content : undefined,
141
+ embeds: 'embeds' in message ? message.embeds : undefined,
142
+ })
143
+ )
144
+
145
+ if ('file' in message) {
146
+ formData.append('file', message.file.file, {
147
+ filename: `${message.file.isSpoiler === true ? 'SPOILER_' : ''}${
148
+ message.file.name
149
+ }`,
150
+ contentType: message.file.contentType,
151
+ })
152
+ }
153
+ }
154
+
155
+ await (this.isDiscordBotOptions(this.options)
156
+ ? this.sendBot(formData)
157
+ : this.sendWebhook(formData))
158
+ }
159
+
160
+ private async sendBot(formData: FormData): Promise<void> {
161
+ if (!this.isDiscordBotOptions(this.options)) {
162
+ throw new Error('Invalid bot options')
163
+ }
164
+
165
+ const response = await axios.post(
166
+ `https://discord.com/api/channels/${this.options}/messages`,
167
+ formData,
168
+ {
169
+ headers: {
170
+ ...formData.getHeaders(),
171
+ Authorization: `Bot ${this.options.token}`,
172
+ },
173
+ validateStatus: () => true,
174
+ }
175
+ )
176
+ if (response.status !== 200) {
177
+ throw new Error(`Discord API returned ${response.status}`)
178
+ }
179
+ }
180
+
181
+ private async sendWebhook(formData: FormData): Promise<void> {
182
+ if (!this.isDiscordWebhookOptions(this.options)) {
183
+ throw new Error('Invalid webhook options')
184
+ }
185
+
186
+ const response = await axios.post(this.options.webhookUrl, formData, {
187
+ headers: {
188
+ ...formData.getHeaders(),
189
+ },
190
+ validateStatus: () => true,
191
+ })
192
+ if (response.status !== 200 && response.status !== 204) {
193
+ throw new Error(`Discord API returned ${response.status}`)
194
+ }
195
+ }
196
+
197
+ private isDiscordBotOptions(
198
+ options: DiscordOptions
199
+ ): options is DiscordBotOptions {
200
+ return (
201
+ 'token' in options &&
202
+ typeof options.token === 'string' &&
203
+ options.token.length > 0 &&
204
+ 'channelId' in options &&
205
+ typeof options.channelId === 'string' &&
206
+ options.channelId.length > 0
207
+ )
208
+ }
209
+
210
+ private isDiscordWebhookOptions(
211
+ options: DiscordOptions
212
+ ): options is DiscordWebhookOptions {
213
+ return (
214
+ 'webhookUrl' in options &&
215
+ typeof options.webhookUrl === 'string' &&
216
+ options.webhookUrl.length > 0
217
+ )
218
+ }
219
+ }
@@ -0,0 +1,33 @@
1
+ import { ConfigFramework, Logger } from '..'
2
+
3
+ export interface Configuration {
4
+ foo: string
5
+ bar: number
6
+ }
7
+
8
+ class ExampleConfiguration extends ConfigFramework<Configuration> {
9
+ protected validates(): { [key: string]: (config: Configuration) => boolean } {
10
+ return {
11
+ // ...Discord.validations, // When using a message transmission to Discord
12
+ 'foo is required': (config) => config.foo !== undefined,
13
+ 'foo is string': (config) => typeof config.foo === 'string',
14
+ 'foo is 3 or more characters': (config) => config.foo.length >= 3,
15
+ 'bar is required': (config) => config.bar !== undefined,
16
+ 'bar is number': (config) => typeof config.bar === 'number',
17
+ }
18
+ }
19
+ }
20
+
21
+ export function exampleConfiguration() {
22
+ const logger = Logger.configure('exampleConfiguration')
23
+ const config = new ExampleConfiguration()
24
+ config.load()
25
+ if (!config.validate()) {
26
+ logger.error('Configuration validation failed')
27
+ logger.error(config.getValidateFailures().join(', '))
28
+ return
29
+ }
30
+
31
+ logger.info(`foo: ${config.get('foo')}`)
32
+ logger.info(`bar: ${config.get('bar')}`)
33
+ }
@@ -0,0 +1,28 @@
1
+ import { Discord, Logger } from '..'
2
+
3
+ export async function exampleDiscord() {
4
+ const logger = Logger.configure('exampleDiscord')
5
+
6
+ const discordWebhookUrl = process.env.DISCORD_WEBHOOK_URL
7
+ if (!discordWebhookUrl) {
8
+ logger.error('DISCORD_WEBHOOK_URL are required')
9
+ return
10
+ }
11
+
12
+ const discord = new Discord({
13
+ webhookUrl: discordWebhookUrl,
14
+ })
15
+
16
+ await discord.sendMessage('Hello world!')
17
+
18
+ await discord.sendMessage({
19
+ embeds: [
20
+ {
21
+ title: 'Hello world!',
22
+ description:
23
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
24
+ color: 0x00_ff_00,
25
+ },
26
+ ],
27
+ })
28
+ }
@@ -0,0 +1,6 @@
1
+ import { Logger } from '..'
2
+
3
+ export function exampleLogger() {
4
+ const logger = Logger.configure('exampleLogger')
5
+ logger.info('Hello world!')
6
+ }