discorb 0.10.2 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ea8e05d77b37b7a269b43dff64422e3d49b08d6c09a639d40dc019baaa26a8c
4
- data.tar.gz: c8ea6f45759aaf22112f6400f7d950fb09f427f98d01302a18094ad3d6a3ae08
3
+ metadata.gz: f0bee36d996a7e3108f2d02247a0d7516ade1811aeb5a354d1d41f509373381d
4
+ data.tar.gz: ce6c4f496b8ed5309cd3798b8cf25e271e77e75617631010d77f5ab3347a26aa
5
5
  SHA512:
6
- metadata.gz: b3f5e77107192aec6943087251c5b61ab1476eb421ddb0297d61178e5146466a5a59c50f4665a2b81c362d927765259142ed1bc08ed9c07e3018102becab3294
7
- data.tar.gz: b88f74d4f3e0f0025f7086e95c2b1e2b96ed19a35a258a72dd29f1814986465ac9fdd8443f920fe8b7dcd610d35871b59354ba8cc6b478bcf883797cd17d463d
6
+ metadata.gz: 514f4b1ce307abe3424fa666a9fab56282e7b3817371411661675507658a42a92c23cda56e6358765fcad25bae69b850e754882576392ddc45da8be08ff3d252
7
+ data.tar.gz: e11b02e78f5029a0e544a0c7a55d79ee42ee22276160ba0f00dad0bd85c2d5d15109fa28c50dbfccbaf61066e293175e6b38357db396aeaba4b3033e38dbf2c4
@@ -0,0 +1,34 @@
1
+ name: Publish Gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@master
15
+
16
+ - name: Set up Ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: "3.0"
20
+
21
+ - name: Setup Release Credentials
22
+ env:
23
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
24
+ run: |
25
+ mkdir -p $HOME/.gem
26
+ touch $HOME/.gem/credentials
27
+ chmod 600 $HOME/.gem/credentials
28
+ echo "---" >$HOME/.gem/credentials
29
+ echo ":github: Bearer ${GITHUB_TOKEN}" >> $HOME/.gem/credentials
30
+ - name: Publish Gem to GitHub Packages
31
+ run: |
32
+ export OWNER=$( echo ${{ github.repository }} | cut -d "/" -f 1 )
33
+ gem build *.gemspec
34
+ gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
data/Changelog.md CHANGED
@@ -2,8 +2,39 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## v0.11
6
+
7
+ ### v0.11.2
8
+
9
+ - Add: Add `setup` event
10
+ - Fix: Fix gateway resuming
11
+ - Add: Add GitHub Packages
12
+
13
+ ### v0.11.1
14
+
15
+ - Improve: Improve rate limit handling
16
+ - Fix: Fix bug in Integration initalization
17
+ - Change: Change log style
18
+ - Add: Support OP code 7
19
+
20
+ ### v0.11.0
21
+
22
+ - Add: Improve documents
23
+ - Add: Implement global rate limits
24
+ - Add: Add support autocomplete
25
+ - Add: Add role icon editting
26
+ - Change: Use `include Discorb::Extension` instead of `< Discorb::Extension`
27
+ - Fix: Fix role operation
28
+
5
29
  ## v0.10
6
30
 
31
+ ### v0.10.3
32
+
33
+ - Add: Support role icons
34
+ - Fix: Fix version order
35
+ - Change: Use `exec` instead of `system` in `discorb run`
36
+ - Add: Add `Extension.loaded`
37
+
7
38
  ### v0.10.2
8
39
 
9
40
  - Change: `discorb init` is now `discorb new`
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ----
10
10
 
11
- discorb is a Discord API wrapper for Ruby.
11
+ discorb is a Discord API wrapper written in Ruby.
12
12
 
13
13
  ## Installation
14
14
 
@@ -98,7 +98,7 @@ end
98
98
  client.run(ENV["DISCORD_BOT_TOKEN"])
99
99
  ```
100
100
 
101
- Note: You must run `discorb setup` before using slash commands.
101
+ Note you must run `discorb setup` before using slash commands.
102
102
 
103
103
  ## Contributing
104
104
 
data/Rakefile CHANGED
@@ -156,7 +156,7 @@ namespace :document do
156
156
  Rake::Task["document:replace:html"].execute
157
157
  Rake::Task["document:replace:css"].execute
158
158
  Rake::Task["document:replace:eol"].execute
159
- tags = `git tag`.force_encoding("utf-8").split("\n")
159
+ tags = `git tag`.force_encoding("utf-8").split("\n").sort_by { |t| t[1..].split(".").map(&:to_i) }
160
160
  tags.each do |tag|
161
161
  sh "git checkout #{tag} -f"
162
162
  iputs "Building #{tag}"
data/discorb.gemspec CHANGED
@@ -13,8 +13,6 @@ Gem::Specification.new do |spec|
13
13
  spec.license = "MIT"
14
14
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
15
15
 
16
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
-
18
16
  spec.metadata["homepage_uri"] = spec.homepage
19
17
  spec.metadata["source_code_uri"] = "https://github.com/discorb-lib/discorb"
20
18
  spec.metadata["changelog_uri"] = "https://discorb-lib.github.io/file.Changelog.html"
@@ -69,6 +69,7 @@ In `options`, hash should be like this:
69
69
  | `:choice` | `Hash{String => String, Integer, Float}` | Type of the option. |
70
70
  | `:default` | `Object` | Default value of the option. |
71
71
  | `:channel_types` | `Array<Class<Discorb::Channel>>` | Type of the channel option. |
72
+ | `:autocomplete` | `Proc` | Autocomplete function. |
72
73
 
73
74
  `choices` should be unspecified if you don't want to use it.
74
75
  `choices` is hash like this:
@@ -107,7 +108,7 @@ In `type`, You must use one of the following:
107
108
  | `:channel` | Channel argument. | None |
108
109
  | `:role` | Role argument. | None |
109
110
 
110
- ### Group Slash Commands
111
+ #### Group Slash Commands
111
112
 
112
113
  To register a group of slash commands, use {Discorb::ApplicationCommand::Handler#slash_group}.
113
114
 
@@ -239,6 +240,31 @@ end
239
240
 
240
241
  Same as above, you can use block for register commands since v0.5.1.
241
242
 
243
+ #### Use Auto Completing
244
+
245
+ Since v0.11.0, you can use auto completion by setting Proc to `:autocomplete` in options.
246
+ The proc will be called with interaction object and the argument.
247
+ The proc should return an hash of the autocomplete result.
248
+
249
+ ```ruby
250
+ client.slash("hello2", "Greet for you", {
251
+ "target" => {
252
+ type: :string,
253
+ description: "Person to greet",
254
+ autocomplete: ->(interaction, target) {
255
+ {
256
+ "You" => interaction.target.to_s
257
+ }
258
+ },
259
+ },
260
+ }) do |interaction, target|
261
+ interaction.post("Hello, #{target}!")
262
+ end
263
+ ```
264
+
265
+ In the above example, `You` will be displayed in the user menu.
266
+ Due to the limitation of Discord API, the proc must return the result in less than 3 second.
267
+
242
268
  ### Register User Context Menu Command
243
269
 
244
270
  ```ruby
File without changes
data/docs/cli/setup.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # discorb setup
4
4
 
5
- This command will setup application commands.
5
+ This command will setup application commands, and call `setup` event.
6
6
 
7
7
  ## Usage
8
8
 
data/docs/cli.md CHANGED
@@ -16,7 +16,7 @@ Currently, discorb has the following commands:
16
16
 
17
17
  | Command | Description |
18
18
  |---------|-------------|
19
- | {file:docs/cli/init.md `init`} | Create a new project. |
19
+ | {file:docs/cli/new.md `new`} | Create a new project. |
20
20
  | {file:docs/cli/irb.md `irb`} | Start an interactive Ruby shell with connected client. |
21
21
  | {file:docs/cli/run.md `run`} | Run a client. |
22
22
  | {file:docs/cli/setup.md `setup`} | Setup application commands. |
data/docs/events.md CHANGED
@@ -88,6 +88,11 @@ Fires when the client is resumed connection.
88
88
  Fires when an error occurs during an event.
89
89
  Defaults to printing the error to stderr, override to handle it yourself.
90
90
 
91
+ #### `setup()`
92
+
93
+ Fires when `discorb setup` is run.
94
+ This is useful for setting up some dependencies, such as the database.
95
+
91
96
  ### Guild events
92
97
 
93
98
  #### `guild_join(guild)`
data/docs/extension.md CHANGED
@@ -2,16 +2,18 @@
2
2
 
3
3
  # Extension
4
4
 
5
- Extension allows you to seperate your code from the main application.
5
+ Extension allows you to seperate events.
6
6
 
7
7
  # @since
8
8
 
9
9
  ## Make a new extension
10
10
 
11
- Make a new class that extends Extension.
11
+ Make a new class that includes Extension.
12
12
 
13
13
  ```ruby
14
- class MyExtension < Discorb::Extension
14
+ class MyExtension
15
+ include Discorb::Extension
16
+
15
17
  # ...
16
18
  end
17
19
  ```
@@ -21,7 +23,9 @@ end
21
23
  Use {Discorb::Extension.event} to register event, or {Discorb::Extension.once_event} to register event only once.
22
24
 
23
25
  ```ruby
24
- class MyExtension < Discorb::Extension
26
+ class MyExtension
27
+ include Discorb::Extension
28
+
25
29
  event :message do |message|
26
30
  # ...
27
31
  end
@@ -39,7 +43,9 @@ Note block will be binded to the extension instance.
39
43
  Use `Discorb::Extension.command` to register command, see {Discorb::ApplicationCommand::Handler} for more information.
40
44
 
41
45
  ```ruby
42
- class MyExtension < Discorb::Extension
46
+ class MyExtension
47
+ include Discorb::Extension
48
+
43
49
  slash("command", "Command") do |interaction|
44
50
  # ...
45
51
  end
@@ -64,7 +70,9 @@ end
64
70
  Use {Discorb::Client#load_extension} to load extension.
65
71
 
66
72
  ```ruby
67
- class MyExtension < Discorb::Extension
73
+ class MyExtension
74
+ include Discorb::Extension
75
+
68
76
  event :message do |message|
69
77
  # ...
70
78
  end
@@ -78,9 +86,45 @@ client.load_extension(MyExtension)
78
86
  You can access {Discorb::Client} from extension with `@client`.
79
87
 
80
88
  ```ruby
81
- class MyExtension < Discorb::Extension
89
+ class MyExtension
90
+ include Discorb::Extension
91
+
82
92
  event :standby do |message|
83
93
  puts "Logged in as #{@client.user}"
84
94
  end
85
95
  end
86
96
  ```
97
+
98
+ ## Receiving Arguments on load
99
+
100
+ You can receive arguments by adding some arguments to `#initialize`.
101
+
102
+ ```ruby
103
+ class MyExtension
104
+ include Discorb::Extension
105
+
106
+ def initialize(client, arg1, arg2)
107
+ super(client)
108
+ # @client = client will also work, but it's not recommended.
109
+ @arg1 = arg1
110
+ @arg2 = arg2
111
+ end
112
+ end
113
+
114
+ client.load_extension(MyExtension, "arg1", "arg2")
115
+
116
+ ```
117
+
118
+ ## Do something on load
119
+
120
+ You can do something on load by overriding `.loaded`. Client and arguments will be passed to it.
121
+
122
+ ```ruby
123
+ class MyExtension
124
+ include Discorb::Extension
125
+
126
+ def self.loaded(client)
127
+ puts "This extension is loaded to #{client}"
128
+ end
129
+ end
130
+ ```
data/docs/faq.md CHANGED
@@ -2,13 +2,25 @@
2
2
 
3
3
  # Fequently asked questions
4
4
 
5
- ## What is `Async::Task`?
5
+ ## What is ...?
6
+
7
+ ### What is `Async::Task`?
6
8
 
7
9
  Async::Task is a object for asynchronous tasks.
8
10
 
9
11
  https://socketry.github.io/async/ for more information.
10
12
 
11
- ## How do I do something with sent messages?
13
+ ### What is `Guild`?
14
+
15
+ It means a `server` of Discord.
16
+
17
+ ### What is difference between `User` and `Member`?
18
+
19
+ `User` is a object for account, `Member` is a object for user in guild.
20
+
21
+ ## How can I ...?
22
+
23
+ ### How can I do something with sent messages?
12
24
 
13
25
  Use `Async::Task#wait` method.
14
26
 
@@ -22,11 +34,12 @@ message = channel.post("Hello world!").wait # => Message
22
34
  message.pin
23
35
  ```
24
36
 
25
- ## How can I send DM to a user?
37
+
38
+ ### How can I send DM to a user?
26
39
 
27
40
  Use {Discorb::User#post} method, {Discorb::User} includes {Discorb::Messageable}.
28
41
 
29
- ## How can I edit status?
42
+ ### How can I edit status?
30
43
 
31
44
  Use {Discorb::Client#update_presence} method.
32
45
 
@@ -47,7 +60,7 @@ client.on :ready do
47
60
  end
48
61
  ```
49
62
 
50
- ## How can I send files?
63
+ ### How can I send files?
51
64
 
52
65
  Use {Discorb::File} class.
53
66
 
@@ -62,6 +75,15 @@ message.channel.post "File!", files: [Discorb::File.new(File.open("./README.md")
62
75
  message.channel.post file: Discorb::File.from_string("Hello world!", "hello.txt")
63
76
  ```
64
77
 
78
+ ### How can I add reactions?
79
+
80
+ Use {Discorb::Message#add_reaction} method.
81
+
82
+ ```ruby
83
+ message.add_reaction Discorb::UnicodeEmoji["🤔"]
84
+ message.add_reaction Discorb::UnicodeEmoji["thinking"]
85
+ ```
86
+
65
87
  # Not fequently asked questions
66
88
 
67
89
  ## How can I pronounce `discorb`?
data/docs/tutorial.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  # Tutorial
4
4
 
5
- Welcome to discorb! This lib allows you to create a discord bot with ease. So, let's get started!
5
+ Welcome to discorb! This lib allows you to create a discord bot with Ruby.
6
+ Let's get started!
6
7
 
7
8
  ## Requirements
8
9
 
@@ -22,6 +23,7 @@ Welcome to discorb! This lib allows you to create a discord bot with ease. So, l
22
23
  - [Sublime Text](https://www.sublimetext.com/)
23
24
  - [Brackets](https://brackets.io/)
24
25
  - [Notepad++](https://notepad-plus-plus.org/)
26
+ - [Vim](https://www.vim.org/)
25
27
  - Git
26
28
  - Bundler
27
29
 
@@ -1,6 +1,8 @@
1
1
  require "discorb"
2
2
 
3
- class MessageExpander < Discorb::Extension
3
+ class MessageExpander
4
+ include Discorb::Extension
5
+
4
6
  @@message_regex = Regexp.new(
5
7
  '(?!<)https://(?:ptb\.|canary\.)?discord(?:app)?\.com/channels/' \
6
8
  "(?<guild>[0-9]{18})/(?<channel>[0-9]{18})/(?<message>[0-9]{18})(?!>)"
@@ -26,6 +26,7 @@ module Discorb
26
26
  # | `:choice` | `Hash{String => String, Integer, Float}` | Type of the option. |
27
27
  # | `:default` | `Object` | Default value of the option. |
28
28
  # | `:channel_types` | `Array<Class<Discorb::Channel>>` | Type of the channel option. |
29
+ # | `:autocomplete` | `Proc` | Autocomplete function. |
29
30
  #
30
31
  # @param [Array<#to_s>, false, nil] guild_ids Guild IDs to set the command to. `false` to global command, `nil` to use default.
31
32
  # @param [Proc] block Command block.
@@ -236,12 +237,12 @@ module Discorb
236
237
  description: value[:description],
237
238
  required: value[:required].nil? ? !value[:optional] : value[:required],
238
239
  }
239
- if value[:choices]
240
- ret[:choices] = value[:choices].map { |t| { name: t[0], value: t[1] } }
241
- end
242
- if value[:channel_types]
243
- ret[:channel_types] = value[:channel_types].map(&:channel_type)
244
- end
240
+
241
+ ret[:choices] = value[:choices].map { |t| { name: t[0], value: t[1] } } if value[:choices]
242
+
243
+ ret[:channel_types] = value[:channel_types].map(&:channel_type) if value[:channel_types]
244
+
245
+ ret[:autocomplete] = !!value[:autocomplete] if value[:autocomplete]
245
246
  ret
246
247
  end
247
248
  {
@@ -401,6 +401,9 @@ module Discorb
401
401
  cmd.define_singleton_method(:extension) { ins.name }
402
402
  @commands << cmd
403
403
  end
404
+
405
+ cls = ins.class
406
+ cls.loaded(self, ...) if cls.respond_to? :loaded
404
407
  @bottom_commands += ins.class.bottom_commands
405
408
  @extensions[ins.class.name] = ins
406
409
  ins
@@ -443,16 +446,7 @@ module Discorb
443
446
  ::File.open(options[:log_file], "a")
444
447
  end
445
448
  @log.level = options[:log_level].to_sym
446
- @log.colorize_log = case options[:log_color]
447
- when nil
448
- if @log.out == $stdout || @log.out == $stderr
449
- true
450
- else
451
- false
452
- end
453
- when true, false
454
- options[:log_color]
455
- end
449
+ @log.colorize_log = options[:log_color] == nil ? @log.out.isatty : options[:log_color]
456
450
  end
457
451
  end
458
452
  start_client(token)
@@ -465,6 +459,10 @@ module Discorb
465
459
  guild_ids = false
466
460
  end
467
461
  setup_commands(token, guild_ids: guild_ids).wait
462
+ @events[:setup]&.each do |event|
463
+ event.call
464
+ end
465
+ self.on_setup if respond_to? :on_setup
468
466
  end
469
467
  end
470
468
 
@@ -490,7 +488,7 @@ module Discorb
490
488
  @close_condition = Async::Condition.new
491
489
  @main_task = Async do
492
490
  @status = :running
493
- connect_gateway(true).wait
491
+ connect_gateway(false).wait
494
492
  rescue
495
493
  @status = :stopped
496
494
  @close_condition.signal
data/lib/discorb/color.rb CHANGED
@@ -130,6 +130,7 @@ module Discorb
130
130
  #
131
131
  # Create a color from a Discord's color.
132
132
  # Currently these colors are supported:
133
+ #
133
134
  # | Color Name | Hexadecimal |
134
135
  # |------------|------------|
135
136
  # | `:teal` | `#1abc9c` |
@@ -4,7 +4,7 @@ module Discorb
4
4
  # @return [String] The API base URL.
5
5
  API_BASE_URL = "https://discord.com/api/v9"
6
6
  # @return [String] The version of discorb.
7
- VERSION = "0.10.2"
7
+ VERSION = "0.11.2"
8
8
  # @return [String] The user agent for the bot.
9
9
  USER_AGENT = "DiscordBot (https://discorb-lib.github.io #{VERSION}) Ruby/#{RUBY_VERSION}"
10
10
 
@@ -214,6 +214,7 @@ module Discorb
214
214
  placeholder: @placeholder,
215
215
  min_values: @min_values,
216
216
  max_values: @max_values,
217
+ disabled: @disabled,
217
218
  }
218
219
  end
219
220
 
@@ -71,9 +71,9 @@ ENV["DISCORB_CLI_TITLE"] = options[:title]
71
71
 
72
72
  if File.exist? script
73
73
  if options[:bundler]
74
- system "bundle exec ruby #{script}"
74
+ exec "bundle exec ruby #{script}"
75
75
  else
76
- system "ruby #{script}"
76
+ exec "ruby #{script}"
77
77
  end
78
78
  else
79
79
  eputs "Could not load script: \e[31m#{script}\e[91m"
@@ -3,15 +3,11 @@
3
3
  module Discorb
4
4
  #
5
5
  # Abstract class to make extension.
6
- # Inherit from this class to make your own extension.
6
+ # Include from this module to make your own extension.
7
7
  # @see file:docs/extension.md
8
8
  # @abstract
9
9
  #
10
- class Extension
11
- extend Discorb::ApplicationCommand::Handler
12
-
13
- @events = {}
14
-
10
+ module Extension
15
11
  def initialize(client)
16
12
  @client = client
17
13
  end
@@ -27,7 +23,12 @@ module Discorb
27
23
  @events = ret
28
24
  end
29
25
 
30
- class << self
26
+ def self.included(base)
27
+ base.extend(ClassMethods)
28
+ end
29
+
30
+ module ClassMethods
31
+ include Discorb::ApplicationCommand::Handler
31
32
  undef setup_commands
32
33
 
33
34
  #
@@ -69,7 +70,7 @@ module Discorb
69
70
  # @private
70
71
  attr_reader :bottom_commands
71
72
 
72
- def inherited(klass)
73
+ def self.extended(klass)
73
74
  klass.instance_variable_set(:@commands, [])
74
75
  klass.instance_variable_set(:@bottom_commands, [])
75
76
  klass.instance_variable_set(:@events, {})
@@ -481,11 +481,10 @@ module Discorb
481
481
  module Handler
482
482
  private
483
483
 
484
- def connect_gateway(first)
484
+ def connect_gateway(reconnect)
485
485
  @log.info "Connecting to gateway."
486
486
  Async do
487
- @http = HTTP.new(self) if first
488
- @first = first
487
+ @http = HTTP.new(self)
489
488
  _, gateway_response = @http.get("/gateway").wait
490
489
  gateway_url = gateway_response[:url]
491
490
  endpoint = Async::HTTP::Endpoint.parse("#{gateway_url}?v=9&encoding=json&compress=zlib-stream",
@@ -495,36 +494,55 @@ module Discorb
495
494
  @connection = connection
496
495
  @zlib_stream = Zlib::Inflate.new(Zlib::MAX_WBITS)
497
496
  @buffer = +""
498
- while (message = @connection.read)
499
- @buffer << message
500
- if message.end_with?((+"\x00\x00\xff\xff").force_encoding("ASCII-8BIT"))
501
- begin
502
- data = @zlib_stream.inflate(@buffer)
503
- @buffer = +""
504
- message = JSON.parse(data, symbolize_names: true)
505
- rescue JSON::ParserError
506
- @buffer = +""
507
- @log.error "Received invalid JSON from gateway."
508
- @log.debug data
509
- else
510
- handle_gateway(message)
497
+ begin
498
+ while (message = @connection.read)
499
+ @buffer << message
500
+ if message.end_with?((+"\x00\x00\xff\xff").force_encoding("ASCII-8BIT"))
501
+ begin
502
+ data = @zlib_stream.inflate(@buffer)
503
+ @buffer = +""
504
+ message = JSON.parse(data, symbolize_names: true)
505
+ rescue JSON::ParserError
506
+ @buffer = +""
507
+ @log.error "Received invalid JSON from gateway."
508
+ @log.debug "#{data}"
509
+ else
510
+ handle_gateway(message, reconnect)
511
+ end
511
512
  end
512
513
  end
514
+ rescue EOFError, Async::Wrapper::Cancelled, Async::Wrapper::WaitError
515
+ # Ignore
513
516
  end
514
517
  end
515
518
  rescue Protocol::WebSocket::ClosedError => e
516
- case e.message
517
- when "Authentication failed."
518
- @tasks.map(&:stop)
519
- raise ClientError.new("Authentication failed."), cause: nil
520
- when "Discord WebSocket requesting client reconnect."
521
- @log.info "Discord WebSocket requesting client reconnect"
522
- connect_gateway(false)
519
+ @tasks.map(&:stop)
520
+ case e.code
521
+ when 4004
522
+ raise ClientError.new("Authentication failed"), cause: nil
523
+ when 4009
524
+ @log.info "Session timed out, reconnecting."
525
+ connect_gateway(true)
526
+ when 4014
527
+ raise ClientError.new("Disallowed intents were specified"), cause: nil
528
+ when 4002, 4003, 4005, 4007
529
+ raise ClientError.new(<<~EOS), cause: e
530
+ Disconnected from gateway, probably due to library issues.
531
+ #{e.message}
532
+
533
+ Please report this to the library issue tracker.
534
+ https://github.com/discorb-lib/discorb/issues
535
+ EOS
536
+ when 1001
537
+ @log.info "Gateway closed with code 1001, reconnecting."
538
+ connect_gateway(true)
523
539
  else
524
- @log.error "Discord WebSocket closed: #{e.message}"
540
+ @log.error "Discord WebSocket closed with code #{e.code}."
541
+ @log.debug "#{e.message}"
525
542
  connect_gateway(false)
526
543
  end
527
- rescue EOFError, Async::Wrapper::Cancelled
544
+ rescue => e
545
+ @log.error "Discord WebSocket error: #{e.message}"
528
546
  connect_gateway(false)
529
547
  end
530
548
  end
@@ -533,18 +551,26 @@ module Discorb
533
551
  def send_gateway(opcode, **value)
534
552
  @connection.write({ op: opcode, d: value }.to_json)
535
553
  @connection.flush
536
- @log.debug "Sent message: #{{ op: opcode, d: value }.to_json.gsub(@token, "[Token]")}"
554
+ @log.debug "Sent message #{{ op: opcode, d: value }.to_json.gsub(@token, "[Token]")}"
537
555
  end
538
556
 
539
- def handle_gateway(payload)
557
+ def handle_gateway(payload, reconnect)
540
558
  Async do |task|
541
559
  data = payload[:d]
542
560
  @last_s = payload[:s] if payload[:s]
543
- @log.debug "Received message with opcode #{payload[:op]} from gateway: #{data}"
561
+ @log.debug "Received message with opcode #{payload[:op]} from gateway:"
562
+ @log.debug "#{payload.to_json.gsub(@token, "[Token]")}"
544
563
  case payload[:op]
545
564
  when 10
546
565
  @heartbeat_interval = data[:heartbeat_interval]
547
- if @first
566
+ if reconnect
567
+ payload = {
568
+ token: @token,
569
+ session_id: @session_id,
570
+ seq: @last_s,
571
+ }
572
+ send_gateway(6, **payload)
573
+ else
548
574
  payload = {
549
575
  token: @token,
550
576
  intents: @intents.value,
@@ -553,33 +579,22 @@ module Discorb
553
579
  }
554
580
  payload[:presence] = @identify_presence if @identify_presence
555
581
  send_gateway(2, **payload)
556
- Async do
557
- sleep 2
558
- next unless @uncached_guilds.nil?
559
-
560
- raise ClientError, "Failed to connect to gateway.\nHint: This usually means that your intents are invalid."
561
- exit 1
562
- end
563
- else
564
- payload = {
565
- token: @token,
566
- session_id: @session_id,
567
- seq: @last_s,
568
- }
569
- send_gateway(6, **payload)
570
582
  end
583
+ when 7
584
+ @log.info "Received opcode 7, reconnecting"
585
+ @tasks.map(&:stop)
571
586
  when 9
572
587
  @log.warn "Received opcode 9, closed connection"
573
588
  @tasks.map(&:stop)
574
589
  if data
575
590
  @log.info "Connection is resumable, reconnecting"
576
591
  @connection.close
577
- connect_gateway(false)
592
+ connect_gateway(true)
578
593
  else
579
594
  @log.info "Connection is not resumable, reconnecting with opcode 2"
580
- sleep(2)
581
595
  @connection.close
582
- connect_gateway(true)
596
+ sleep(2)
597
+ connect_gateway(false)
583
598
  end
584
599
  when 11
585
600
  @log.debug "Received opcode 11"
@@ -1024,7 +1039,7 @@ module Discorb
1024
1039
  if respond_to?("event_" + event_name.downcase)
1025
1040
  __send__("event_" + event_name.downcase, data)
1026
1041
  else
1027
- @log.debug "Received unknown event: #{event_name}\n#{data.inspect}"
1042
+ @log.debug "#{event_name}\n#{data.inspect}"
1028
1043
  end
1029
1044
  end
1030
1045
  end
data/lib/discorb/http.rb CHANGED
@@ -36,13 +36,7 @@ module Discorb
36
36
  resp = http.get(get_path(path), get_headers(headers, "", audit_log_reason), **kwargs)
37
37
  data = get_response_data(resp)
38
38
  @ratelimit_handler.save("GET", path, resp)
39
- test_error(if resp.code == "429"
40
- @client.log.warn "Ratelimit exceeded for #{path}, trying again in #{data[:retry_after]} seconds."
41
- task.sleep(data[:retry_after])
42
- get(path, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
43
- else
44
- [resp, data]
45
- end)
39
+ handle_response(:patch, resp, data, path, nil, headers, audit_log_reason, kwargs)
46
40
  end
47
41
  end
48
42
 
@@ -67,12 +61,7 @@ module Discorb
67
61
  resp = http.post(get_path(path), get_body(body), get_headers(headers, body, audit_log_reason), **kwargs)
68
62
  data = get_response_data(resp)
69
63
  @ratelimit_handler.save("POST", path, resp)
70
- test_error(if resp.code == "429"
71
- task.sleep(data[:retry_after])
72
- post(path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
73
- else
74
- [resp, data]
75
- end)
64
+ handle_response(:post, resp, data, path, body, headers, audit_log_reason, kwargs)
76
65
  end
77
66
  end
78
67
 
@@ -97,12 +86,7 @@ module Discorb
97
86
  resp = http.patch(get_path(path), get_body(body), get_headers(headers, body, audit_log_reason), **kwargs)
98
87
  data = get_response_data(resp)
99
88
  @ratelimit_handler.save("PATCH", path, resp)
100
- test_error(if resp.code == "429"
101
- task.sleep(data[:retry_after])
102
- patch(path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
103
- else
104
- [resp, data]
105
- end)
89
+ handle_response(:patch, resp, data, path, body, headers, audit_log_reason, kwargs)
106
90
  end
107
91
  end
108
92
 
@@ -127,12 +111,7 @@ module Discorb
127
111
  resp = http.put(get_path(path), get_body(body), get_headers(headers, body, audit_log_reason), **kwargs)
128
112
  data = get_response_data(resp)
129
113
  @ratelimit_handler.save("PUT", path, resp)
130
- test_error(if resp.code == "429"
131
- task.sleep(data[:retry_after])
132
- put(path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
133
- else
134
- [resp, data]
135
- end)
114
+ handle_response(:put, resp, data, path, body, headers, audit_log_reason, kwargs)
136
115
  end
137
116
  end
138
117
 
@@ -151,17 +130,12 @@ module Discorb
151
130
  # @raise [Discorb::HTTPError] The request was failed.
152
131
  #
153
132
  def delete(path, headers: nil, audit_log_reason: nil, **kwargs)
154
- Async do |task|
133
+ Async do
155
134
  @ratelimit_handler.wait("DELETE", path)
156
135
  resp = http.delete(get_path(path), get_headers(headers, "", audit_log_reason))
157
136
  data = get_response_data(resp)
158
137
  @ratelimit_handler.save("DELETE", path, resp)
159
- test_error(if resp.code == "429"
160
- task.sleep(data[:retry_after])
161
- delete(path, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
162
- else
163
- [resp, data]
164
- end)
138
+ handle_response(:delete, resp, data, path, nil, headers, audit_log_reason, kwargs)
165
139
  end
166
140
  end
167
141
 
@@ -198,9 +172,16 @@ module Discorb
198
172
 
199
173
  private
200
174
 
201
- def test_error(ary)
202
- resp, data = *ary
175
+ def handle_response(method, resp, data, path, body, headers, audit_log_reason, kwargs)
203
176
  case resp.code
177
+ when "429"
178
+ @client.log.info("Rate limit exceeded for #{method} #{path}, waiting #{data[:retry_after]} seconds")
179
+ sleep(data[:retry_after])
180
+ if body
181
+ __send__(method, path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
182
+ else
183
+ __send__(method, path, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
184
+ end
204
185
  when "400"
205
186
  raise BadRequestError.new(resp, data)
206
187
  when "401"
@@ -247,15 +228,19 @@ module Discorb
247
228
  end
248
229
 
249
230
  def get_response_data(resp)
250
- if resp["Via"].nil? && resp.code == "429"
251
- raise CloudFlareBanError.new(resp, @client)
231
+ begin
232
+ data = JSON.parse(resp.body, symbolize_names: true)
233
+ rescue JSON::ParserError, TypeError
234
+ if resp.body.nil? || resp.body.empty?
235
+ data = nil
236
+ else
237
+ data = resp.body
238
+ end
252
239
  end
253
- rd = resp.body
254
- if rd.nil? || rd.empty?
255
- nil
256
- else
257
- JSON.parse(rd, symbolize_names: true)
240
+ if resp["Via"].nil? && resp.code == "429" && data.is_a?(String)
241
+ raise CloudFlareBanError.new(resp, @client)
258
242
  end
243
+ data
259
244
  end
260
245
 
261
246
  def http
@@ -63,7 +63,7 @@ module Discorb
63
63
  #
64
64
  def delete!(reason: nil)
65
65
  Async do
66
- @client.http.delete("/guilds/#{@guild}/integrations/#{@id}", reason: reason).wait
66
+ @client.http.delete("/guilds/#{@guild}/integrations/#{@id}", audit_log_reason: reason).wait
67
67
  end
68
68
  end
69
69
 
@@ -80,7 +80,7 @@ module Discorb
80
80
  @enable_emoticons = data[:enable_emoticons]
81
81
  @expire_behavior = self.class.expire_behavior[data[:expire_behavior]]
82
82
  @expire_grace_period = data[:expire_grace_period]
83
- @user = @client.users[data[:user].to_i]
83
+ @user = @client.users[data[:user][:id]] or Discorb::User.new(@client, data[:user])
84
84
  @account = Account.new(data[:account])
85
85
  @subscriber_count = data[:subscriber_count]
86
86
  @revoked = data[:revoked]
@@ -364,7 +364,7 @@ module Discorb
364
364
  end
365
365
 
366
366
  unless (command = @client.bottom_commands.find { |c| c.to_s == name && c.type_raw == 1 })
367
- @client.log.warn "Unknown command name #{name}, ignoreing"
367
+ @client.log.warn "Unknown command name #{name}, ignoring"
368
368
  next
369
369
  end
370
370
 
@@ -451,6 +451,89 @@ module Discorb
451
451
  end
452
452
  end
453
453
 
454
+ #
455
+ # Represents auto complete interaction.
456
+ #
457
+ class AutoComplete < Interaction
458
+ @interaction_type = 4
459
+ @interaction_name = :auto_complete
460
+
461
+ # @private
462
+ def _set_data(data)
463
+ super
464
+ Sync do
465
+ name = data[:name]
466
+ options = nil
467
+ if (option = data[:options]&.first)
468
+ case option[:type]
469
+ when 1
470
+ name += " #{option[:name]}"
471
+ options = option[:options]
472
+ when 2
473
+ name += " #{option[:name]}"
474
+ if (option_sub = option[:options]&.first)
475
+ if option_sub[:type] == 1
476
+ name += " #{option_sub[:name]}"
477
+ options = option_sub[:options]
478
+ else
479
+ options = option[:options]
480
+ end
481
+ end
482
+ else
483
+ options = data[:options]
484
+ end
485
+ end
486
+
487
+ unless (command = @client.bottom_commands.find { |c| c.to_s == name && c.type_raw == 1 })
488
+ @client.log.warn "Unknown command name #{name}, ignoring"
489
+ next
490
+ end
491
+
492
+ option_map = command.options.map { |k, v| [k.to_s, v[:default]] }.to_h
493
+ options ||= []
494
+ options.each_with_index do |option|
495
+ val = case option[:type]
496
+ when 3, 4, 5, 10
497
+ option[:value]
498
+ when 6
499
+ guild.members[option[:value]] || guild.fetch_member(option[:value]).wait
500
+ when 7
501
+ guild.channels[option[:value]] || guild.fetch_channels.wait.find { |channel| channel.id == option[:value] }
502
+ when 8
503
+ guild.roles[option[:value]] || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
504
+ when 9
505
+ guild.members[option[:value]] || guild.roles[option[:value]] || guild.fetch_member(option[:value]).wait || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
506
+ end
507
+ option_map[option[:name]] = val
508
+ end
509
+ focused_index = options.find_index { |o| o[:focused] }
510
+ val = command.options.values[focused_index][:autocomplete]&.call(self, *command.options.map { |k, v| option_map[k.to_s] })
511
+ send_complete_result(val)
512
+ end
513
+ end
514
+
515
+ # @private
516
+ def send_complete_result(val)
517
+ @client.http.post("/interactions/#{@id}/#{@token}/callback", {
518
+ type: 8,
519
+ data: {
520
+ choices: val.map do |vk, vv|
521
+ {
522
+ name: vk,
523
+ value: vv,
524
+ }
525
+ end,
526
+ },
527
+ }).wait
528
+ rescue Discorb::NotFoundError
529
+ @client.log.warn "Failed to send auto complete result, This may be caused by the suggestion is taking too long (over 3 seconds) to respond", fallback: $stderr
530
+ end
531
+
532
+ class << self
533
+ alias make_interaction new
534
+ end
535
+ end
536
+
454
537
  #
455
538
  # Represents a message component interaction.
456
539
  # @abstract
data/lib/discorb/log.rb CHANGED
@@ -65,10 +65,11 @@ module Discorb
65
65
  return
66
66
  end
67
67
 
68
+ time = Time.now.iso8601
68
69
  if @colorize_log
69
- @out.puts("\e[2;90m[#{Time.now.iso8601}] #{color}#{name}\e[m -- #{message}")
70
+ @out.puts("\e[90m#{time}\e[0m #{color}#{name.ljust(5)}\e[0m #{message}")
70
71
  else
71
- @out.puts("[#{Time.now.iso8601}] #{name} -- #{message}")
72
+ @out.puts("#{time} #{name.ljust(5)} #{message}")
72
73
  end
73
74
  end
74
75
  end
@@ -71,21 +71,12 @@ module Discorb
71
71
  _set_data(user_data, member_data)
72
72
  end
73
73
 
74
- #
75
- # Format the member to `@name` style.
76
- #
77
- # @return [String] The formatted member.
78
- #
79
- def to_s
80
- "@#{name}"
81
- end
82
-
83
74
  #
84
75
  # Format the member to `Username#Discriminator` style.
85
76
  #
86
77
  # @return [String] The formatted member.
87
78
  #
88
- def to_s_user
79
+ def to_s
89
80
  "#{username}##{discriminator}"
90
81
  end
91
82
 
@@ -94,7 +94,7 @@ module Discorb
94
94
  #
95
95
  def delete_message!(message_id, reason: nil)
96
96
  Async do
97
- @client.http.delete("/channels/#{channel_id.wait}/messages/#{message_id}", reason: reason).wait
97
+ @client.http.delete("/channels/#{channel_id.wait}/messages/#{message_id}", audit_log_reason: reason).wait
98
98
  end
99
99
  end
100
100
 
@@ -9,8 +9,9 @@ module Discorb
9
9
  # @private
10
10
  def initialize(client)
11
11
  @client = client
12
- @ratelimit_hash = {}
13
- @path_ratelimit_hash = {}
12
+ @current_ratelimits = {}
13
+ @path_ratelimit_bucket = {}
14
+ @global = false
14
15
  end
15
16
 
16
17
  #
@@ -22,18 +23,26 @@ module Discorb
22
23
  def wait(method, path)
23
24
  return if path.start_with?("https://")
24
25
 
25
- return unless hash = @path_ratelimit_hash[method + path]
26
+ if @global
27
+ time = b[:reset_at] - Time.now.to_f
28
+ @client.log.info("global rate limit reached, waiting #{time} seconds")
29
+ sleep(time)
30
+ @global = false
31
+ end
32
+
33
+ return unless hash = @path_ratelimit_bucket[method + path]
26
34
 
27
- return unless b = @ratelimit_hash[hash]
35
+ return unless b = @current_ratelimits[hash]
28
36
 
29
- if b[:reset_at] < Time.now.to_i
30
- @ratelimit_hash.delete(hash)
37
+ if b[:reset_at] < Time.now.to_f
38
+ @current_ratelimits.delete(hash)
31
39
  return
32
40
  end
33
41
  return if b[:remaining] > 0
34
42
 
35
- @client.log.info("Ratelimit reached, waiting for #{b[:reset_at] - Time.now.to_i} seconds")
36
- sleep(b[:reset_at] - Time.now.to_i)
43
+ time = b[:reset_at] - Time.now.to_f
44
+ @client.log.info("rate limit for #{method} #{path} reached, waiting #{time} seconds")
45
+ sleep(time)
37
46
  end
38
47
 
39
48
  #
@@ -44,12 +53,15 @@ module Discorb
44
53
  # @param [Net::HTTPResponse] resp The response.
45
54
  #
46
55
  def save(method, path, resp)
56
+ if resp["X-Ratelimit-Global"] == "true"
57
+ @global = Time.now.to_f + JSON.parse(resp.body, symbolize_names: true)[:retry_after]
58
+ end
47
59
  return unless resp["X-RateLimit-Remaining"]
48
60
 
49
- @path_ratelimit_hash[method + path] = resp["X-RateLimit-Bucket"]
50
- @ratelimit_hash[resp["X-RateLimit-Bucket"]] = {
61
+ @path_ratelimit_bucket[method + path] = resp["X-RateLimit-Bucket"]
62
+ @current_ratelimits[resp["X-RateLimit-Bucket"]] = {
51
63
  remaining: resp["X-RateLimit-Remaining"].to_i,
52
- reset_at: resp["X-RateLimit-Reset"].to_i,
64
+ reset_at: Time.now.to_f + resp["X-RateLimit-Reset-After"].to_f,
53
65
  }
54
66
  end
55
67
  end
data/lib/discorb/role.rb CHANGED
@@ -26,6 +26,10 @@ module Discorb
26
26
  # @return [Boolean] Whether the role is a default role.
27
27
  attr_reader :mentionable
28
28
  alias mentionable? mentionable
29
+ # @return [Discorb::Asset, nil] The icon of the role.
30
+ attr_reader :custom_icon
31
+ # @return [Discorb::Emoji, nil] The emoji of the role.
32
+ attr_reader :emoji
29
33
 
30
34
  # @!attribute [r] mention
31
35
  # @return [String] The mention of the role.
@@ -33,6 +37,8 @@ module Discorb
33
37
  # @return [Boolean] Whether the role has a color.
34
38
  # @!attribute [r] tag
35
39
  # @return [Discorb::Role::Tag] The tag of the role.
40
+ # @!attribute [r] icon
41
+ # @return [Discorb::Asset, Discorb::Emoji] The icon of the role.
36
42
 
37
43
  include Comparable
38
44
 
@@ -44,6 +50,10 @@ module Discorb
44
50
  _set_data(data)
45
51
  end
46
52
 
53
+ def icon
54
+ @custom_icon || @emoji
55
+ end
56
+
47
57
  #
48
58
  # Compares two roles by their position.
49
59
  #
@@ -88,7 +98,7 @@ module Discorb
88
98
  #
89
99
  def move(position, reason: nil)
90
100
  Async do
91
- @client.http.patch("/guilds/#{@guild_id}/roles", { id: @id, position: position }, reason: reason).wait
101
+ @client.http.patch("/guilds/#{@guild.id}/roles", { id: @id, position: position }, audit_log_reason: reason).wait
92
102
  end
93
103
  end
94
104
 
@@ -103,9 +113,10 @@ module Discorb
103
113
  # @param [Discorb::Color] color The new color of the role.
104
114
  # @param [Boolean] hoist Whether the role should be hoisted.
105
115
  # @param [Boolean] mentionable Whether the role should be mentionable.
116
+ # @param [Discorb::Image, Discorb::UnicodeEmoji] icon The new icon or emoji of the role.
106
117
  # @param [String] reason The reason for editing the role.
107
118
  #
108
- def edit(name: :unset, position: :unset, color: :unset, hoist: :unset, mentionable: :unset, reason: nil)
119
+ def edit(name: :unset, position: :unset, color: :unset, hoist: :unset, mentionable: :unset, icon: :unset, reason: nil)
109
120
  Async do
110
121
  payload = {}
111
122
  payload[:name] = name if name != :unset
@@ -113,7 +124,14 @@ module Discorb
113
124
  payload[:color] = color.to_i if color != :unset
114
125
  payload[:hoist] = hoist if hoist != :unset
115
126
  payload[:mentionable] = mentionable if mentionable != :unset
116
- @client.http.patch("/guilds/#{@guild_id}/roles/#{@id}", payload, reason: reason).wait
127
+ if icon != :unset
128
+ if icon.is_a?(Discorb::Image)
129
+ payload[:icon] = icon.to_s
130
+ else
131
+ payload[:unicode_emoji] = icon.to_s
132
+ end
133
+ end
134
+ @client.http.patch("/guilds/#{@guild.id}/roles/#{@id}", payload, audit_log_reason: reason).wait
117
135
  end
118
136
  end
119
137
 
@@ -126,7 +144,7 @@ module Discorb
126
144
  #
127
145
  def delete!(reason: nil)
128
146
  Async do
129
- @client.http.delete("/guilds/#{@guild_id}/roles/#{@id}", reason: reason).wait
147
+ @client.http.delete("/guilds/#{@guild.id}/roles/#{@id}", audit_log_reason: reason).wait
130
148
  end
131
149
  end
132
150
 
@@ -182,6 +200,8 @@ module Discorb
182
200
  @managed = data[:managed]
183
201
  @mentionable = data[:mentionable]
184
202
  @tags = data[:tags] || {}
203
+ @custom_icon = data[:icon] ? Asset.new(self, data[:icon], path: "role-icons/#{@id}") : nil
204
+ @emoji = data[:unicode_emoji] ? UnicodeEmoji.new(data[:unicode_emoji]) : nil
185
205
  @guild.roles[@id] = self unless data[:no_cache]
186
206
  @data.update(data)
187
207
  end
@@ -193,7 +193,7 @@ module Discorb
193
193
  #
194
194
  def delete!(reason: nil)
195
195
  Async do
196
- @client.http.delete("/stage-instances/#{@channel_id}", reason: reason).wait
196
+ @client.http.delete("/stage-instances/#{@channel_id}", audit_log_reason: reason).wait
197
197
  self
198
198
  end
199
199
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: discorb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - sevenc-nanashi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-03 00:00:00.000000000 Z
11
+ date: 2021-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -79,6 +79,7 @@ files:
79
79
  - ".github/ISSUE_TEMPLATE/feature_request.md"
80
80
  - ".github/workflows/build_main.yml"
81
81
  - ".github/workflows/build_version.yml"
82
+ - ".github/workflows/package_register.yml"
82
83
  - ".gitignore"
83
84
  - ".yardopts"
84
85
  - Changelog.md
@@ -103,8 +104,8 @@ files:
103
104
  - docs/assets/08_hello_once.png
104
105
  - docs/assets/unused_ping_pong.png
105
106
  - docs/cli.md
106
- - docs/cli/init.md
107
107
  - docs/cli/irb.md
108
+ - docs/cli/new.md
108
109
  - docs/cli/run.md
109
110
  - docs/cli/setup.md
110
111
  - docs/events.md
@@ -191,7 +192,6 @@ homepage: https://github.com/discorb-lib/discorb
191
192
  licenses:
192
193
  - MIT
193
194
  metadata:
194
- allowed_push_host: https://rubygems.org
195
195
  homepage_uri: https://github.com/discorb-lib/discorb
196
196
  source_code_uri: https://github.com/discorb-lib/discorb
197
197
  changelog_uri: https://discorb-lib.github.io/file.Changelog.html