async-matrix 1.0.0 → 1.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/discord-api-spec/openapi.json +40404 -0
  4. data/lib/async/discord/api/path_tree.rb +130 -0
  5. data/lib/async/discord/api.rb +156 -0
  6. data/lib/async/discord/client.rb +286 -0
  7. data/lib/async/discord/error.rb +88 -0
  8. data/lib/async/discord/gateway.rb +362 -0
  9. data/lib/async/discord.rb +16 -0
  10. data/lib/async/matrix/api/chain.rb +14 -8
  11. data/lib/async/matrix/application_service/config/vivify.rb +3 -0
  12. data/lib/async/matrix/application_service/event.rb +9 -20
  13. data/lib/async/matrix/application_service/server.rb +2 -2
  14. data/lib/async/matrix/bridge/discord/db/connection.rb +143 -0
  15. data/lib/async/matrix/bridge/discord/db/file.rb +120 -0
  16. data/lib/async/matrix/bridge/discord/db/guild.rb +122 -0
  17. data/lib/async/matrix/bridge/discord/db/message.rb +162 -0
  18. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +14 -0
  19. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +14 -0
  20. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +23 -0
  21. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +19 -0
  22. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +20 -0
  23. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +19 -0
  24. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +18 -0
  25. data/lib/async/matrix/bridge/discord/db/portal.rb +152 -0
  26. data/lib/async/matrix/bridge/discord/db/puppet.rb +130 -0
  27. data/lib/async/matrix/bridge/discord/db/reaction.rb +167 -0
  28. data/lib/async/matrix/bridge/discord/db/user.rb +114 -0
  29. data/lib/async/matrix/bridge/discord/db.rb +140 -0
  30. data/lib/async/matrix/double_puppet_client.rb +84 -0
  31. data/lib/async/matrix/schema.rb +2 -2
  32. data/lib/async/matrix/server.rb +1 -0
  33. data/lib/async/matrix/version.rb +1 -1
  34. data/lib/async/matrix.rb +2 -0
  35. metadata +67 -1
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "bundler/setup"
7
+ require "async/matrix"
8
+
9
+ module Async
10
+ module Matrix
11
+ module Bridge
12
+ module Discord
13
+ module DB
14
+ # Caches uploaded media to avoid re-uploading the same Discord
15
+ # attachment to the Matrix homeserver.
16
+ #
17
+ # Uses a composite primary key of (url, encrypted) because the same
18
+ # source URL may be uploaded both encrypted and unencrypted to
19
+ # different portal rooms.
20
+ #
21
+ # cached = CachedFile.create(
22
+ # url: "https://cdn.discordapp.com/...",
23
+ # encrypted: false,
24
+ # mxc: "mxc://example.com/abc"
25
+ # )
26
+ #
27
+ class CachedFile < Sequel::Model
28
+ unrestrict_primary_key
29
+
30
+ def validate
31
+ super
32
+ errors.add(:url, "cannot be empty") if url.nil? || url.empty?
33
+ errors.add(:mxc, "cannot be empty") if mxc.nil? || mxc.empty?
34
+ end
35
+
36
+ # Look up a cached file by source URL and encryption status.
37
+ def self.by_url(url, encrypted: false)
38
+ first(url: url, encrypted: encrypted)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ test do
48
+ describe "Async::Matrix::Bridge::Discord::DB::CachedFile" do
49
+ def setup_db
50
+ db = Sequel.sqlite
51
+ Sequel::Migrator.run(db, File.join(__dir__, "migrations"))
52
+ db
53
+ end
54
+
55
+ it "creates and retrieves a cached file" do
56
+ db = setup_db
57
+ Async::Matrix::Bridge::Discord::DB::CachedFile.dataset = db[:files]
58
+
59
+ cached = Async::Matrix::Bridge::Discord::DB::CachedFile.create(
60
+ url: "https://cdn.discordapp.com/attachments/123/456/image.png",
61
+ mxc: "mxc://example.com/abc",
62
+ mime_type: "image/png",
63
+ size: 102400,
64
+ width: 800,
65
+ height: 600
66
+ )
67
+ cached.url.should.include "discordapp.com"
68
+ cached.mxc.should == "mxc://example.com/abc"
69
+ cached.mime_type.should == "image/png"
70
+ cached.width.should == 800
71
+ cached.height.should == 600
72
+ db.disconnect
73
+ end
74
+
75
+ it "looks up by URL and encryption status" do
76
+ db = setup_db
77
+ Async::Matrix::Bridge::Discord::DB::CachedFile.dataset = db[:files]
78
+
79
+ Async::Matrix::Bridge::Discord::DB::CachedFile.create(
80
+ url: "https://example.com/file.png", encrypted: false,
81
+ mxc: "mxc://x/plain"
82
+ )
83
+ Async::Matrix::Bridge::Discord::DB::CachedFile.create(
84
+ url: "https://example.com/file.png", encrypted: true,
85
+ mxc: "mxc://x/encrypted",
86
+ decryption_info: '{"key":"abc"}'
87
+ )
88
+
89
+ plain = Async::Matrix::Bridge::Discord::DB::CachedFile.by_url("https://example.com/file.png")
90
+ plain.mxc.should == "mxc://x/plain"
91
+
92
+ enc = Async::Matrix::Bridge::Discord::DB::CachedFile.by_url("https://example.com/file.png", encrypted: true)
93
+ enc.mxc.should == "mxc://x/encrypted"
94
+ enc.decryption_info.should == '{"key":"abc"}'
95
+ db.disconnect
96
+ end
97
+
98
+ it "defaults encrypted to false" do
99
+ db = setup_db
100
+ Async::Matrix::Bridge::Discord::DB::CachedFile.dataset = db[:files]
101
+
102
+ cached = Async::Matrix::Bridge::Discord::DB::CachedFile.create(
103
+ url: "https://example.com/a.jpg", mxc: "mxc://x/y"
104
+ )
105
+ cached.encrypted.should == false
106
+ db.disconnect
107
+ end
108
+
109
+ it "validates required fields" do
110
+ db = setup_db
111
+ Async::Matrix::Bridge::Discord::DB::CachedFile.dataset = db[:files]
112
+
113
+ f = Async::Matrix::Bridge::Discord::DB::CachedFile.new(url: "", mxc: "")
114
+ f.valid?.should == false
115
+ f.errors[:url].should.not.be.empty
116
+ f.errors[:mxc].should.not.be.empty
117
+ db.disconnect
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "bundler/setup"
7
+ require "async/matrix"
8
+
9
+ module Async
10
+ module Matrix
11
+ module Bridge
12
+ module Discord
13
+ module DB
14
+ # Maps a Discord guild (server) to a Matrix Space room.
15
+ #
16
+ # Bridging modes control how aggressively the bridge creates portals:
17
+ # BRIDGE_NOTHING = 0 — never bridge
18
+ # BRIDGE_IF_PORTAL_EXISTS = 1 — only bridge existing portals
19
+ # BRIDGE_CREATE_ON_MESSAGE = 2 — create portals on first message
20
+ # BRIDGE_EVERYTHING = 3 — proactively create all portals
21
+ #
22
+ # guild = Guild.create(discord_id: "999", name: "My Server", bridging_mode: 3)
23
+ # guild.portals # => [Portal, ...]
24
+ #
25
+ class Guild < Sequel::Model
26
+ unrestrict_primary_key
27
+
28
+ BRIDGE_NOTHING = 0
29
+ BRIDGE_IF_PORTAL_EXISTS = 1
30
+ BRIDGE_CREATE_ON_MESSAGE = 2
31
+ BRIDGE_EVERYTHING = 3
32
+
33
+ one_to_many :portals,
34
+ class: "Async::Matrix::Bridge::Discord::DB::Portal",
35
+ key: :discord_guild_id,
36
+ primary_key: :discord_id
37
+
38
+ def validate
39
+ super
40
+ errors.add(:discord_id, "cannot be empty") if discord_id.nil? || discord_id.empty?
41
+ end
42
+
43
+ def bridge_nothing?
44
+ bridging_mode == BRIDGE_NOTHING
45
+ end
46
+
47
+ def bridge_everything?
48
+ bridging_mode == BRIDGE_EVERYTHING
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ test do
58
+ describe "Async::Matrix::Bridge::Discord::DB::Guild" do
59
+ def setup_db
60
+ db = Sequel.sqlite
61
+ Sequel::Migrator.run(db, File.join(__dir__, "migrations"))
62
+ db
63
+ end
64
+
65
+ it "creates and retrieves a guild" do
66
+ db = setup_db
67
+ Async::Matrix::Bridge::Discord::DB::Guild.dataset = db[:guilds]
68
+
69
+ guild = Async::Matrix::Bridge::Discord::DB::Guild.create(
70
+ discord_id: "999",
71
+ name: "Test Guild",
72
+ mxid: "!space:example.com"
73
+ )
74
+ guild.discord_id.should == "999"
75
+ guild.name.should == "Test Guild"
76
+ guild.mxid.should == "!space:example.com"
77
+ db.disconnect
78
+ end
79
+
80
+ it "defaults bridging_mode to 0 (nothing)" do
81
+ db = setup_db
82
+ Async::Matrix::Bridge::Discord::DB::Guild.dataset = db[:guilds]
83
+
84
+ guild = Async::Matrix::Bridge::Discord::DB::Guild.create(discord_id: "100")
85
+ guild.bridging_mode.should == 0
86
+ guild.bridge_nothing?.should == true
87
+ guild.bridge_everything?.should == false
88
+ db.disconnect
89
+ end
90
+
91
+ it "supports all bridging modes" do
92
+ db = setup_db
93
+ Async::Matrix::Bridge::Discord::DB::Guild.dataset = db[:guilds]
94
+
95
+ guild = Async::Matrix::Bridge::Discord::DB::Guild.create(discord_id: "200", bridging_mode: 3)
96
+ guild.bridge_everything?.should == true
97
+ guild.bridge_nothing?.should == false
98
+ db.disconnect
99
+ end
100
+
101
+ it "validates discord_id is present" do
102
+ db = setup_db
103
+ Async::Matrix::Bridge::Discord::DB::Guild.dataset = db[:guilds]
104
+
105
+ guild = Async::Matrix::Bridge::Discord::DB::Guild.new(discord_id: "")
106
+ guild.valid?.should == false
107
+ guild.errors[:discord_id].should.not.be.empty
108
+ db.disconnect
109
+ end
110
+
111
+ it "enforces unique mxid" do
112
+ db = setup_db
113
+ Async::Matrix::Bridge::Discord::DB::Guild.dataset = db[:guilds]
114
+
115
+ Async::Matrix::Bridge::Discord::DB::Guild.create(discord_id: "300", mxid: "!a:x")
116
+ lambda {
117
+ Async::Matrix::Bridge::Discord::DB::Guild.create(discord_id: "301", mxid: "!a:x")
118
+ }.should.raise(Sequel::UniqueConstraintViolation)
119
+ db.disconnect
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "bundler/setup"
7
+ require "async/matrix"
8
+
9
+ module Async
10
+ module Matrix
11
+ module Bridge
12
+ module Discord
13
+ module DB
14
+ # Maps a Discord message to one or more Matrix events.
15
+ #
16
+ # A single Discord message may produce multiple Matrix events (one per
17
+ # attachment), distinguished by discord_attachment_id. The composite
18
+ # unique index on (discord_id, discord_attachment_id, discord_channel_id,
19
+ # discord_channel_receiver) ensures no duplicates.
20
+ #
21
+ # msg = Message.create(
22
+ # discord_id: "msg1", discord_channel_id: "ch1",
23
+ # discord_sender: "user1", mxid: "$evt1", timestamp: 1234567890
24
+ # )
25
+ # msg.portal # => Portal
26
+ #
27
+ class Message < Sequel::Model
28
+ many_to_one :portal,
29
+ class: "Async::Matrix::Bridge::Discord::DB::Portal",
30
+ key: [:discord_channel_id, :discord_channel_receiver],
31
+ primary_key: [:discord_id, :receiver]
32
+
33
+ def validate
34
+ super
35
+ errors.add(:discord_id, "cannot be empty") if discord_id.nil? || discord_id.empty?
36
+ errors.add(:mxid, "cannot be empty") if mxid.nil? || mxid.empty?
37
+ errors.add(:discord_sender, "cannot be empty") if discord_sender.nil? || discord_sender.empty?
38
+ end
39
+
40
+ # Find all parts of a Discord message (text + attachments).
41
+ def self.by_discord_id(discord_id, channel_id, receiver = "")
42
+ where(
43
+ discord_id: discord_id,
44
+ discord_channel_id: channel_id,
45
+ discord_channel_receiver: receiver
46
+ ).order(:discord_attachment_id).all
47
+ end
48
+
49
+ # Find a message by its Matrix event ID.
50
+ def self.by_mxid(mxid)
51
+ first(mxid: mxid)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ test do
61
+ describe "Async::Matrix::Bridge::Discord::DB::Message" do
62
+ def setup_db
63
+ db = Sequel.sqlite
64
+ Sequel::Migrator.run(db, File.join(__dir__, "migrations"))
65
+ db
66
+ end
67
+
68
+ def set_datasets(db)
69
+ Async::Matrix::Bridge::Discord::DB::Message.dataset = db[:messages]
70
+ Async::Matrix::Bridge::Discord::DB::Portal.dataset = db[:portals]
71
+ end
72
+
73
+ it "creates and retrieves a message" do
74
+ db = setup_db
75
+ set_datasets(db)
76
+
77
+ msg = Async::Matrix::Bridge::Discord::DB::Message.create(
78
+ discord_id: "msg1",
79
+ discord_channel_id: "ch1",
80
+ discord_sender: "user1",
81
+ mxid: "$evt1",
82
+ timestamp: 1234567890
83
+ )
84
+ msg.discord_id.should == "msg1"
85
+ msg.mxid.should == "$evt1"
86
+ msg.discord_attachment_id.should == ""
87
+ db.disconnect
88
+ end
89
+
90
+ it "supports multi-part messages with attachments" do
91
+ db = setup_db
92
+ set_datasets(db)
93
+
94
+ Async::Matrix::Bridge::Discord::DB::Message.create(
95
+ discord_id: "msg2", discord_attachment_id: "",
96
+ discord_channel_id: "ch1", discord_sender: "u1",
97
+ mxid: "$text", timestamp: 100
98
+ )
99
+ Async::Matrix::Bridge::Discord::DB::Message.create(
100
+ discord_id: "msg2", discord_attachment_id: "att1",
101
+ discord_channel_id: "ch1", discord_sender: "u1",
102
+ mxid: "$img1", timestamp: 100
103
+ )
104
+ Async::Matrix::Bridge::Discord::DB::Message.create(
105
+ discord_id: "msg2", discord_attachment_id: "att2",
106
+ discord_channel_id: "ch1", discord_sender: "u1",
107
+ mxid: "$img2", timestamp: 100
108
+ )
109
+
110
+ parts = Async::Matrix::Bridge::Discord::DB::Message.by_discord_id("msg2", "ch1")
111
+ parts.length.should == 3
112
+ parts.map(&:mxid).should == ["$text", "$img1", "$img2"]
113
+ db.disconnect
114
+ end
115
+
116
+ it "finds by Matrix event ID" do
117
+ db = setup_db
118
+ set_datasets(db)
119
+
120
+ Async::Matrix::Bridge::Discord::DB::Message.create(
121
+ discord_id: "msg3", discord_channel_id: "ch1",
122
+ discord_sender: "u1", mxid: "$find_me", timestamp: 200
123
+ )
124
+
125
+ found = Async::Matrix::Bridge::Discord::DB::Message.by_mxid("$find_me")
126
+ found.should.not.be.nil
127
+ found.discord_id.should == "msg3"
128
+ db.disconnect
129
+ end
130
+
131
+ it "enforces unique composite key" do
132
+ db = setup_db
133
+ set_datasets(db)
134
+
135
+ Async::Matrix::Bridge::Discord::DB::Message.create(
136
+ discord_id: "msg4", discord_attachment_id: "",
137
+ discord_channel_id: "ch1", discord_channel_receiver: "",
138
+ discord_sender: "u1", mxid: "$a", timestamp: 300
139
+ )
140
+ lambda {
141
+ Async::Matrix::Bridge::Discord::DB::Message.create(
142
+ discord_id: "msg4", discord_attachment_id: "",
143
+ discord_channel_id: "ch1", discord_channel_receiver: "",
144
+ discord_sender: "u1", mxid: "$b", timestamp: 300
145
+ )
146
+ }.should.raise(Sequel::UniqueConstraintViolation)
147
+ db.disconnect
148
+ end
149
+
150
+ it "validates required fields" do
151
+ db = setup_db
152
+ set_datasets(db)
153
+
154
+ msg = Async::Matrix::Bridge::Discord::DB::Message.new(discord_id: "", mxid: "", discord_sender: "")
155
+ msg.valid?.should == false
156
+ msg.errors[:discord_id].should.not.be.empty
157
+ msg.errors[:mxid].should.not.be.empty
158
+ msg.errors[:discord_sender].should.not.be.empty
159
+ db.disconnect
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:users) do
6
+ String :mxid, primary_key: true
7
+ String :discord_id, unique: true
8
+ String :discord_token
9
+ String :management_room
10
+ String :space_room
11
+ String :dm_space_room
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:guilds) do
6
+ String :discord_id, primary_key: true
7
+ String :mxid, unique: true
8
+ String :name
9
+ String :avatar_hash
10
+ String :avatar_url
11
+ Integer :bridging_mode, default: 0, null: false
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:portals) do
6
+ String :discord_id, null: false
7
+ String :receiver, null: false, default: ""
8
+ String :mxid, unique: true
9
+ String :discord_guild_id
10
+ String :name
11
+ String :topic
12
+ String :avatar_hash
13
+ String :avatar_url
14
+ TrueClass :encrypted, default: false, null: false
15
+ Integer :channel_type, default: 0, null: false
16
+ String :relay_webhook_id
17
+ String :relay_webhook_secret
18
+
19
+ primary_key [:discord_id, :receiver]
20
+ foreign_key [:discord_guild_id], :guilds, key: :discord_id
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:puppets) do
6
+ String :discord_id, primary_key: true
7
+ String :name
8
+ String :avatar_hash
9
+ String :avatar_url
10
+ String :username
11
+ String :global_name
12
+ TrueClass :is_bot, default: false, null: false
13
+ TrueClass :is_webhook, default: false, null: false
14
+ TrueClass :contact_info_set, default: false, null: false
15
+ String :custom_mxid
16
+ String :access_token
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:messages) do
6
+ primary_key :id
7
+ String :discord_id, null: false
8
+ String :discord_attachment_id, null: false, default: ""
9
+ String :discord_channel_id, null: false
10
+ String :discord_channel_receiver, null: false, default: ""
11
+ String :discord_sender, null: false
12
+ String :mxid, null: false
13
+ Bignum :timestamp, null: false
14
+ String :discord_thread_id
15
+
16
+ unique [:discord_id, :discord_attachment_id, :discord_channel_id, :discord_channel_receiver]
17
+ index :mxid
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:reactions) do
6
+ primary_key :id
7
+ String :discord_message_id, null: false
8
+ String :discord_sender, null: false
9
+ String :discord_emoji_name, null: false
10
+ String :mxid, null: false
11
+ String :discord_channel_id, null: false
12
+ String :discord_channel_receiver, null: false, default: ""
13
+ String :discord_thread_id
14
+
15
+ unique [:discord_message_id, :discord_sender, :discord_emoji_name]
16
+ index :mxid
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:files) do
6
+ String :url, null: false
7
+ TrueClass :encrypted, null: false, default: false
8
+ String :mxc, null: false
9
+ String :mime_type
10
+ Bignum :size
11
+ Integer :width
12
+ Integer :height
13
+ String :decryption_info
14
+
15
+ primary_key [:url, :encrypted]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "bundler/setup"
7
+ require "async/matrix"
8
+
9
+ module Async
10
+ module Matrix
11
+ module Bridge
12
+ module Discord
13
+ module DB
14
+ # Maps a Discord channel to a Matrix room.
15
+ #
16
+ # Uses a composite primary key of (discord_id, receiver). For guild
17
+ # channels, receiver is an empty string. For DMs, receiver is the
18
+ # Discord user ID of the bridge user who owns the portal.
19
+ #
20
+ # portal = Portal.create(discord_id: "chan123", receiver: "", mxid: "!room:x")
21
+ # portal.guild # => Guild or nil
22
+ # portal.messages # => [Message, ...]
23
+ #
24
+ class Portal < Sequel::Model
25
+ unrestrict_primary_key
26
+
27
+ many_to_one :guild,
28
+ class: "Async::Matrix::Bridge::Discord::DB::Guild",
29
+ key: :discord_guild_id,
30
+ primary_key: :discord_id
31
+
32
+ one_to_many :messages,
33
+ class: "Async::Matrix::Bridge::Discord::DB::Message",
34
+ key: [:discord_channel_id, :discord_channel_receiver],
35
+ primary_key: [:discord_id, :receiver]
36
+
37
+ def validate
38
+ super
39
+ errors.add(:discord_id, "cannot be empty") if discord_id.nil? || discord_id.empty?
40
+ end
41
+
42
+ # Is this a DM portal? (has a receiver)
43
+ def dm?
44
+ receiver && !receiver.empty?
45
+ end
46
+
47
+ # Is this a guild channel portal?
48
+ def guild_channel?
49
+ !dm?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ test do
59
+ describe "Async::Matrix::Bridge::Discord::DB::Portal" do
60
+ def setup_db
61
+ db = Sequel.sqlite
62
+ Sequel::Migrator.run(db, File.join(__dir__, "migrations"))
63
+ db
64
+ end
65
+
66
+ def set_datasets(db)
67
+ Async::Matrix::Bridge::Discord::DB::Portal.dataset = db[:portals]
68
+ Async::Matrix::Bridge::Discord::DB::Guild.dataset = db[:guilds]
69
+ Async::Matrix::Bridge::Discord::DB::Message.dataset = db[:messages]
70
+ end
71
+
72
+ it "creates a guild channel portal" do
73
+ db = setup_db
74
+ set_datasets(db)
75
+
76
+ portal = Async::Matrix::Bridge::Discord::DB::Portal.create(
77
+ discord_id: "chan1", receiver: "", mxid: "!room:x", name: "general"
78
+ )
79
+ portal.discord_id.should == "chan1"
80
+ portal.receiver.should == ""
81
+ portal.guild_channel?.should == true
82
+ portal.dm?.should == false
83
+ db.disconnect
84
+ end
85
+
86
+ it "creates a DM portal with receiver" do
87
+ db = setup_db
88
+ set_datasets(db)
89
+
90
+ portal = Async::Matrix::Bridge::Discord::DB::Portal.create(
91
+ discord_id: "dm1", receiver: "user123", mxid: "!dm:x"
92
+ )
93
+ portal.dm?.should == true
94
+ portal.guild_channel?.should == false
95
+ db.disconnect
96
+ end
97
+
98
+ it "finds by composite primary key" do
99
+ db = setup_db
100
+ set_datasets(db)
101
+
102
+ Async::Matrix::Bridge::Discord::DB::Portal.create(discord_id: "c1", receiver: "")
103
+ Async::Matrix::Bridge::Discord::DB::Portal.create(discord_id: "c1", receiver: "u1")
104
+
105
+ found = Async::Matrix::Bridge::Discord::DB::Portal["c1", ""]
106
+ found.should.not.be.nil
107
+ found.receiver.should == ""
108
+
109
+ found2 = Async::Matrix::Bridge::Discord::DB::Portal["c1", "u1"]
110
+ found2.receiver.should == "u1"
111
+ db.disconnect
112
+ end
113
+
114
+ it "associates with a guild" do
115
+ db = setup_db
116
+ set_datasets(db)
117
+
118
+ guild = Async::Matrix::Bridge::Discord::DB::Guild.create(discord_id: "g_assoc", name: "Guild")
119
+ portal = Async::Matrix::Bridge::Discord::DB::Portal.create(
120
+ discord_id: "c_assoc", receiver: "", discord_guild_id: "g_assoc"
121
+ )
122
+ # Query through the db directly to avoid races with parallel tests
123
+ # that rebind the class-level dataset on Sequel models.
124
+ portal.discord_guild_id.should == "g_assoc"
125
+ linked_guild = db[:guilds].where(discord_id: portal.discord_guild_id).first
126
+ linked_guild[:discord_id].should == "g_assoc"
127
+ linked_portals = db[:portals].where(discord_guild_id: guild.discord_id).all
128
+ linked_portals.length.should == 1
129
+ db.disconnect
130
+ end
131
+
132
+ it "enforces unique mxid" do
133
+ db = setup_db
134
+ set_datasets(db)
135
+
136
+ Async::Matrix::Bridge::Discord::DB::Portal.create(discord_id: "a", receiver: "", mxid: "!r:x")
137
+ lambda {
138
+ Async::Matrix::Bridge::Discord::DB::Portal.create(discord_id: "b", receiver: "", mxid: "!r:x")
139
+ }.should.raise(Sequel::UniqueConstraintViolation)
140
+ db.disconnect
141
+ end
142
+
143
+ it "defaults encrypted to false" do
144
+ db = setup_db
145
+ set_datasets(db)
146
+
147
+ portal = Async::Matrix::Bridge::Discord::DB::Portal.create(discord_id: "e1", receiver: "")
148
+ portal.encrypted.should == false
149
+ db.disconnect
150
+ end
151
+ end
152
+ end