feishu_notifier 0.1.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 45fef57b6d5c69eed564a4625e58fa3a60c38808ec9de9526a5a0bc6e624ba1a
4
+ data.tar.gz: 2ed23eaceaf3595525b136070210c607e828ce1b6a2bbe6d74ba4fd17704672f
5
+ SHA512:
6
+ metadata.gz: 80367f0000f9db8c102d07f7393b83e4b93a3b2176d1c545801c4c4bc291b1c158545fd16056b3e92eecb4a2fe2b0073e320aa56ed337008aa7c2261086e46bf
7
+ data.tar.gz: 6a0c3b51e0603ebff1c78c0e45f9dad169a12f0f669334ad46e4708860cca45ce04abbf8b1d70c651bf7932d2327432ceb12cee81873a59723831ab0673319d6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Yang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # Feishu Notifier
2
+
3
+ Ruby gem and CLI for sending Feishu app notifications. It supports plain text messages, custom interactive cards, existing CardKit card IDs, and existing card template IDs.
4
+
5
+ Repository: https://github.com/Youngv/feishu-notifier-ruby
6
+
7
+ ## Features
8
+
9
+ - Sends messages through Feishu `im/v1/messages`.
10
+ - Uses `receive_id_type=user_id`.
11
+ - Reads app credentials from `.env`, environment variables, or `--config`.
12
+ - Provides built-in card levels: `info`, `debug`, and `error`.
13
+ - Can be used as a CLI or as a Ruby library.
14
+
15
+ ## Requirements
16
+
17
+ - Ruby 3.2 or newer.
18
+ - This project is developed with Ruby 3.4.7.
19
+ - A Feishu app with permission to send IM messages.
20
+ - A reachable Feishu recipient `user_id`.
21
+
22
+ ## Configuration
23
+
24
+ Create a `.env` file:
25
+
26
+ ```bash
27
+ cp .env.example .env
28
+ ```
29
+
30
+ Required variables:
31
+
32
+ ```bash
33
+ FEISHU_APP_ID=cli_xxx
34
+ FEISHU_APP_SECRET=your_app_secret
35
+ FEISHU_USER_ID=your_user_id
36
+ FEISHU_BASE_URL=https://open.feishu.cn
37
+ ```
38
+
39
+ The local `.env` file is ignored by git. Do not commit real app secrets.
40
+
41
+ ## Development Setup
42
+
43
+ ```bash
44
+ cd /root/feishu-notifier-ruby
45
+ source /usr/local/rvm/scripts/rvm
46
+ rvm use ruby-3.4.7
47
+ bundle install
48
+ ```
49
+
50
+ Run tests:
51
+
52
+ ```bash
53
+ bundle exec rake test
54
+ ```
55
+
56
+ ## CLI Usage
57
+
58
+ Run from this repository:
59
+
60
+ ```bash
61
+ bin/feishu-notifier --level info --text "Hello from Ruby"
62
+ ```
63
+
64
+ Run after installing the gem:
65
+
66
+ ```bash
67
+ feishu-notifier --config /root/feishu-notifier-ruby/.env --level error --text "Something failed"
68
+ ```
69
+
70
+ If the target project has its own `.env`, run `feishu-notifier` from that directory without `--config`.
71
+
72
+ ### Plain Text
73
+
74
+ ```bash
75
+ bin/feishu-notifier --text "Hello from Ruby"
76
+ ```
77
+
78
+ ### Custom Cards
79
+
80
+ The `--level` option sends a custom interactive card. Available levels:
81
+
82
+ - `info`: blue header, default title `Info`
83
+ - `debug`: grey header, default title `Debug`
84
+ - `error`: red header, default title `Error`
85
+
86
+ ```bash
87
+ bin/feishu-notifier --level info --text "Normal notification"
88
+ bin/feishu-notifier --level debug --text "Debug details"
89
+ bin/feishu-notifier --level error --text "Something failed"
90
+ ```
91
+
92
+ Override the card title:
93
+
94
+ ```bash
95
+ bin/feishu-notifier --level error --title "Deploy failed" --text "Rollback required"
96
+ ```
97
+
98
+ ### Existing Cards and Templates
99
+
100
+ Send an existing CardKit card by card ID:
101
+
102
+ ```bash
103
+ bin/feishu-notifier --card-id CARD_ID
104
+ ```
105
+
106
+ Send an existing card template by template ID:
107
+
108
+ ```bash
109
+ bin/feishu-notifier --template-id TEMPLATE_ID
110
+ ```
111
+
112
+ ### Recipient Override
113
+
114
+ ```bash
115
+ bin/feishu-notifier --user-id your_user_id --level info --text "Hello"
116
+ ```
117
+
118
+ ### Dry Run
119
+
120
+ Use `--dry-run` to print the request summary without calling Feishu:
121
+
122
+ ```bash
123
+ bin/feishu-notifier --dry-run --level error --title "Gem check" --text "Installed executable works"
124
+ ```
125
+
126
+ ## Ruby Library Usage
127
+
128
+ Add the gem to another Ruby project, or install the local gem first.
129
+
130
+ ```ruby
131
+ require "feishu_notifier"
132
+
133
+ FeishuNotifier::EnvFile.load("/root/feishu-notifier-ruby/.env")
134
+ config = FeishuNotifier::Config.from_env
135
+ client = FeishuNotifier::Client.new(config: config)
136
+
137
+ card = FeishuNotifier::Card.custom(
138
+ level: "error",
139
+ title: "Task failed",
140
+ text: "Check logs"
141
+ )
142
+
143
+ client.send_card(card)
144
+ ```
145
+
146
+ Send plain text:
147
+
148
+ ```ruby
149
+ client.send_text("Hello from Ruby")
150
+ ```
151
+
152
+ ## Build and Install
153
+
154
+ Build the gem:
155
+
156
+ ```bash
157
+ gem build feishu_notifier.gemspec
158
+ ```
159
+
160
+ Install the generated gem:
161
+
162
+ ```bash
163
+ gem install ./feishu_notifier-0.1.1.gem
164
+ ```
165
+
166
+ ## Project Layout
167
+
168
+ - `bin/feishu-notifier`: CLI executable.
169
+ - `lib/feishu_notifier/client.rb`: Feishu API client.
170
+ - `lib/feishu_notifier/card.rb`: Card content builders.
171
+ - `lib/feishu_notifier/config.rb`: Environment-based configuration.
172
+ - `lib/feishu_notifier/env_file.rb`: Minimal `.env` loader.
173
+ - `lib/feishu_notifier/version.rb`: Gem version.
174
+ - `test/`: Minitest coverage.
175
+
176
+ ## Permissions
177
+
178
+ The Feishu app must be enabled in the target tenant and have permission to send IM messages. The recipient must be reachable by `user_id`.
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "json"
4
+ require "optparse"
5
+
6
+ begin
7
+ require "feishu_notifier"
8
+ rescue LoadError
9
+ require_relative "../lib/feishu_notifier"
10
+ end
11
+
12
+ options = {
13
+ config: ENV.fetch("FEISHU_CONFIG", ".env"),
14
+ dry_run: false,
15
+ message_type: nil,
16
+ level: "info"
17
+ }
18
+
19
+ parser = OptionParser.new do |opts|
20
+ opts.banner = "Usage: feishu-notifier --text MESSAGE | --card-id CARD_ID | --template-id TEMPLATE_ID [options]"
21
+
22
+ opts.on("-t", "--text MESSAGE", "Text message to send") { |value| options[:text] = value }
23
+ opts.on("--card", "Send an interactive card built from --text") { options[:message_type] = "interactive" }
24
+ opts.on("--level LEVEL", "Card level: info, debug, or error") do |value|
25
+ options[:level] = value
26
+ options[:message_type] = "interactive"
27
+ end
28
+ opts.on("--card-id CARD_ID", "Send an existing Feishu CardKit card") do |value|
29
+ options[:card_id] = value
30
+ options[:message_type] = "interactive"
31
+ end
32
+ opts.on("--template-id TEMPLATE_ID", "Send an existing Feishu card template") do |value|
33
+ options[:template_id] = value
34
+ options[:message_type] = "interactive"
35
+ end
36
+ opts.on("--title TITLE", "Card title when using --card") { |value| options[:title] = value }
37
+ opts.on("-u", "--user-id USER_ID", "Override FEISHU_USER_ID") { |value| options[:user_id] = value }
38
+ opts.on("-c", "--config PATH", "Load environment variables from PATH") { |value| options[:config] = value }
39
+ opts.on("--dry-run", "Print request summary without calling Feishu") { options[:dry_run] = true }
40
+ opts.on("-h", "--help", "Print help") do
41
+ puts opts
42
+ exit
43
+ end
44
+ end
45
+
46
+ parser.parse!
47
+
48
+ card_id = options[:card_id].to_s.strip
49
+ template_id = options[:template_id].to_s.strip
50
+ message = options[:text].to_s.strip
51
+ options[:message_type] ||= "text"
52
+ abort parser.to_s if card_id.empty? && template_id.empty? && message.empty?
53
+
54
+ FeishuNotifier::EnvFile.load(options[:config])
55
+ ENV["FEISHU_USER_ID"] = options[:user_id] if options[:user_id]
56
+
57
+ config = FeishuNotifier::Config.from_env
58
+ content = if !card_id.empty?
59
+ FeishuNotifier::Card.by_id(card_id)
60
+ elsif !template_id.empty?
61
+ FeishuNotifier::Card.template(template_id)
62
+ elsif options[:message_type] == "interactive"
63
+ FeishuNotifier::Card.custom(title: options[:title], text: message, level: options[:level])
64
+ else
65
+ { text: message }
66
+ end
67
+
68
+ if options[:dry_run]
69
+ puts JSON.pretty_generate(
70
+ {
71
+ app_id: config.app_id,
72
+ user_id: config.user_id,
73
+ endpoint: "#{config.base_url}/open-apis/im/v1/messages?receive_id_type=user_id",
74
+ msg_type: options[:message_type],
75
+ content: content
76
+ }
77
+ )
78
+ exit
79
+ end
80
+
81
+ client = FeishuNotifier::Client.new(config: config)
82
+ if options[:message_type] == "interactive"
83
+ client.send_card(content)
84
+ else
85
+ client.send_text(message)
86
+ end
87
+ puts "#{options[:message_type]} message sent to #{config.user_id}"
@@ -0,0 +1,71 @@
1
+ module FeishuNotifier
2
+ module Card
3
+ LEVELS = {
4
+ "info" => {
5
+ title: "Info",
6
+ template: "blue"
7
+ },
8
+ "debug" => {
9
+ title: "Debug",
10
+ template: "grey"
11
+ },
12
+ "error" => {
13
+ title: "Error",
14
+ template: "red"
15
+ }
16
+ }.freeze
17
+
18
+ module_function
19
+
20
+ def by_id(card_id)
21
+ {
22
+ type: "card",
23
+ data: {
24
+ card_id: card_id
25
+ }
26
+ }
27
+ end
28
+
29
+ def template(template_id, variables: {})
30
+ {
31
+ type: "template",
32
+ data: {
33
+ template_id: template_id,
34
+ template_variable: variables
35
+ }
36
+ }
37
+ end
38
+
39
+ def custom(title: nil, text:, level: "info")
40
+ style = LEVELS.fetch(level) do
41
+ raise ArgumentError, "Unsupported card level: #{level}. Expected one of: #{LEVELS.keys.join(", ")}"
42
+ end
43
+
44
+ {
45
+ config: {
46
+ wide_screen_mode: true
47
+ },
48
+ header: {
49
+ template: style.fetch(:template),
50
+ title: {
51
+ tag: "plain_text",
52
+ content: title.to_s.empty? ? style.fetch(:title) : title
53
+ }
54
+ },
55
+ elements: [
56
+ {
57
+ tag: "div",
58
+ text: {
59
+ tag: "lark_md",
60
+ content: text
61
+ }
62
+ }
63
+ ]
64
+ }
65
+ end
66
+
67
+ def simple(title:, text:)
68
+ custom(title: title, text: text, level: "info")
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,88 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module FeishuNotifier
6
+ class Client
7
+ TOKEN_PATH = "/open-apis/auth/v3/tenant_access_token/internal".freeze
8
+ MESSAGE_PATH = "/open-apis/im/v1/messages".freeze
9
+
10
+ def initialize(config:, http: Net::HTTP)
11
+ @config = config
12
+ @http = http
13
+ end
14
+
15
+ def send_text(text, user_id: @config.user_id)
16
+ send_message(
17
+ user_id: user_id,
18
+ msg_type: "text",
19
+ content: { text: text }
20
+ )
21
+ end
22
+
23
+ def send_card(card, user_id: @config.user_id)
24
+ send_message(
25
+ user_id: user_id,
26
+ msg_type: "interactive",
27
+ content: card
28
+ )
29
+ end
30
+
31
+ def tenant_access_token
32
+ response = post_json(
33
+ TOKEN_PATH,
34
+ {
35
+ app_id: @config.app_id,
36
+ app_secret: @config.app_secret
37
+ }
38
+ )
39
+
40
+ token = response["tenant_access_token"].to_s
41
+ raise ApiError, "Feishu token response did not include tenant_access_token" if token.empty?
42
+
43
+ token
44
+ end
45
+
46
+ private
47
+
48
+ def send_message(user_id:, msg_type:, content:)
49
+ token = tenant_access_token
50
+ post_json(
51
+ "#{MESSAGE_PATH}?receive_id_type=user_id",
52
+ {
53
+ receive_id: user_id,
54
+ msg_type: msg_type,
55
+ content: JSON.generate(content)
56
+ },
57
+ headers: { "Authorization" => "Bearer #{token}" }
58
+ )
59
+ end
60
+
61
+ def post_json(path, payload, headers: {})
62
+ uri = URI.join(@config.base_url, path)
63
+ request = Net::HTTP::Post.new(uri)
64
+ request["Content-Type"] = "application/json; charset=utf-8"
65
+ headers.each { |key, value| request[key] = value }
66
+ request.body = JSON.generate(payload)
67
+
68
+ response = @http.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |connection|
69
+ connection.request(request)
70
+ end
71
+
72
+ parse_response(response)
73
+ end
74
+
75
+ def parse_response(response)
76
+ body = JSON.parse(response.body)
77
+ code = body.fetch("code", 0)
78
+ return body if response.is_a?(Net::HTTPSuccess) && code.to_i.zero?
79
+
80
+ message = body["msg"] || body["message"] || response.message
81
+ raise ApiError, "Feishu API error: HTTP #{response.code}, code #{code}, #{message}"
82
+ rescue JSON::ParserError
83
+ raise ApiError, "Feishu API returned non-JSON response: HTTP #{response.code}"
84
+ end
85
+ end
86
+
87
+ class ApiError < StandardError; end
88
+ end
@@ -0,0 +1,21 @@
1
+ module FeishuNotifier
2
+ Config = Data.define(:app_id, :app_secret, :user_id, :base_url) do
3
+ DEFAULT_BASE_URL = "https://open.feishu.cn".freeze
4
+
5
+ def self.from_env(env = ENV)
6
+ new(
7
+ app_id: fetch_required(env, "FEISHU_APP_ID"),
8
+ app_secret: fetch_required(env, "FEISHU_APP_SECRET"),
9
+ user_id: fetch_required(env, "FEISHU_USER_ID"),
10
+ base_url: env.fetch("FEISHU_BASE_URL", DEFAULT_BASE_URL)
11
+ )
12
+ end
13
+
14
+ def self.fetch_required(env, key)
15
+ value = env[key].to_s.strip
16
+ raise ArgumentError, "Missing required environment variable: #{key}" if value.empty?
17
+
18
+ value
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ module FeishuNotifier
2
+ module EnvFile
3
+ module_function
4
+
5
+ def load(path)
6
+ return unless File.file?(path)
7
+
8
+ File.readlines(path, chomp: true).each do |line|
9
+ stripped = line.strip
10
+ next if stripped.empty? || stripped.start_with?("#")
11
+
12
+ key, value = stripped.split("=", 2)
13
+ next if key.nil? || value.nil? || key.empty?
14
+
15
+ ENV[key] = unquote(value.strip)
16
+ end
17
+ end
18
+
19
+ def unquote(value)
20
+ if (value.start_with?('"') && value.end_with?('"')) ||
21
+ (value.start_with?("'") && value.end_with?("'"))
22
+ value[1...-1]
23
+ else
24
+ value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module FeishuNotifier
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ require_relative "feishu_notifier/version"
2
+ require_relative "feishu_notifier/card"
3
+ require_relative "feishu_notifier/client"
4
+ require_relative "feishu_notifier/config"
5
+ require_relative "feishu_notifier/env_file"
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: feishu_notifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Victor Yang
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.25'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.2'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.2'
40
+ description: A small Ruby client and CLI for sending Feishu app notifications with
41
+ text and custom card levels.
42
+ executables:
43
+ - feishu-notifier
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - LICENSE
48
+ - README.md
49
+ - bin/feishu-notifier
50
+ - lib/feishu_notifier.rb
51
+ - lib/feishu_notifier/card.rb
52
+ - lib/feishu_notifier/client.rb
53
+ - lib/feishu_notifier/config.rb
54
+ - lib/feishu_notifier/env_file.rb
55
+ - lib/feishu_notifier/version.rb
56
+ homepage: https://github.com/Youngv/feishu-notifier-ruby
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ rubygems_mfa_required: 'true'
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.2.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.9
76
+ specification_version: 4
77
+ summary: Send Feishu text and card notifications from Ruby.
78
+ test_files: []