libertree-model 0.9.11

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/lib/libertree/compat.rb +29 -0
  3. data/lib/libertree/console.rb +17 -0
  4. data/lib/libertree/db.rb +37 -0
  5. data/lib/libertree/embedder.rb +69 -0
  6. data/lib/libertree/embedding/custom-providers.rb +148 -0
  7. data/lib/libertree/model/account-settings.rb +14 -0
  8. data/lib/libertree/model/account.rb +487 -0
  9. data/lib/libertree/model/chat-message.rb +75 -0
  10. data/lib/libertree/model/comment-like.rb +68 -0
  11. data/lib/libertree/model/comment.rb +164 -0
  12. data/lib/libertree/model/contact-list.rb +81 -0
  13. data/lib/libertree/model/embed-cache.rb +6 -0
  14. data/lib/libertree/model/file.rb +6 -0
  15. data/lib/libertree/model/forest.rb +82 -0
  16. data/lib/libertree/model/group-member.rb +13 -0
  17. data/lib/libertree/model/group.rb +47 -0
  18. data/lib/libertree/model/has-display-text.rb +34 -0
  19. data/lib/libertree/model/has-searchable-text.rb +19 -0
  20. data/lib/libertree/model/ignored-member.rb +13 -0
  21. data/lib/libertree/model/invitation.rb +6 -0
  22. data/lib/libertree/model/is-remote-or-local.rb +30 -0
  23. data/lib/libertree/model/job.rb +93 -0
  24. data/lib/libertree/model/member.rb +222 -0
  25. data/lib/libertree/model/message.rb +154 -0
  26. data/lib/libertree/model/node.rb +22 -0
  27. data/lib/libertree/model/node_affiliation.rb +14 -0
  28. data/lib/libertree/model/node_subscription.rb +23 -0
  29. data/lib/libertree/model/notification.rb +71 -0
  30. data/lib/libertree/model/pool-post.rb +17 -0
  31. data/lib/libertree/model/pool.rb +172 -0
  32. data/lib/libertree/model/post-hidden.rb +21 -0
  33. data/lib/libertree/model/post-like.rb +72 -0
  34. data/lib/libertree/model/post-revision.rb +9 -0
  35. data/lib/libertree/model/post.rb +735 -0
  36. data/lib/libertree/model/profile.rb +25 -0
  37. data/lib/libertree/model/remote-storage-connection.rb +6 -0
  38. data/lib/libertree/model/river.rb +249 -0
  39. data/lib/libertree/model/server.rb +35 -0
  40. data/lib/libertree/model/session-account.rb +10 -0
  41. data/lib/libertree/model/url-expansion.rb +6 -0
  42. data/lib/libertree/model.rb +48 -0
  43. data/lib/libertree/query.rb +167 -0
  44. data/lib/libertree/render.rb +28 -0
  45. metadata +198 -0
@@ -0,0 +1,222 @@
1
+ module Libertree
2
+ module Model
3
+ class Member < Sequel::Model(:members)
4
+
5
+ def after_create
6
+ super
7
+ self.distribute
8
+ end
9
+
10
+ def after_update
11
+ super
12
+ self.distribute
13
+ end
14
+
15
+ def before_destroy
16
+ # TODO: expand later for more granularity:
17
+ # - only abandon (not delete) posts and comments
18
+ # - only empty posts if they contain a discussion
19
+ # - only empty and anonymise comments
20
+ # - etc.
21
+ if self.local?
22
+ Libertree::Model::Job.create_for_forests(
23
+ {
24
+ task: 'request:MEMBER-DELETE',
25
+ params: { 'username' => self.account.username, }
26
+ }
27
+ )
28
+ end
29
+ super
30
+ end
31
+
32
+ def distribute
33
+ return if ! self.local?
34
+ Libertree::Model::Job.create_for_forests(
35
+ { task: 'request:MEMBER',
36
+ params: { 'username' => self.account.username }
37
+ }
38
+ )
39
+ end
40
+
41
+ def local?
42
+ ! self.account.nil?
43
+ end
44
+
45
+ def account
46
+ @account ||= Account[self.account_id]
47
+ end
48
+
49
+ # TODO: DB: association
50
+ #many_to_one :server
51
+ def server
52
+ @server ||= Server[self.server_id]
53
+ end
54
+ alias :tree :server
55
+
56
+ def name_display
57
+ @name_display ||= ( profile && profile.name_display || self.username )
58
+ end
59
+
60
+ def handle
61
+ if self.username && self.server
62
+ self.username + "@#{self.server.name_display}"
63
+ elsif account
64
+ account.username + "@#{Server.own_domain}"
65
+ end
66
+ end
67
+
68
+ def self.with_handle(h)
69
+ if h =~ /^(.+?)@(.+?)$/
70
+ username = $1
71
+ host = $2
72
+ if host == Server.own_domain
73
+ local = true
74
+ end
75
+ else
76
+ username = h
77
+ local = true
78
+ end
79
+
80
+ if local
81
+ self.qualify.
82
+ join(:accounts, :id=>:account_id).
83
+ where(:accounts__username => username).
84
+ limit(1).
85
+ first
86
+ else
87
+ # TODO: servers.name_given is no longer used. Remove it after
88
+ # migrating user rivers/contact lists etc.
89
+ self.qualify.
90
+ join(:servers, :id=>:server_id).
91
+ where(:members__username => username).
92
+ where(Sequel.or(:servers__domain => host, :servers__name_given => host)).
93
+ limit(1).
94
+ first
95
+ end
96
+ end
97
+
98
+ # TODO: this is a temporary helper because with_handle no longer includes display name matches.
99
+ # This will eventually be removed when river :from queries no longer accept display names.
100
+ def self.with_display_name(name)
101
+ self.qualify.
102
+ join(:profiles, :member_id=>:id).
103
+ where(:profiles__name_display => name).
104
+ limit(1).
105
+ first
106
+ end
107
+
108
+ def username
109
+ if val = super
110
+ val
111
+ elsif a = self.account
112
+ a.username
113
+ end
114
+ end
115
+
116
+ # TODO: DB: association
117
+ def profile
118
+ @profile ||= Profile[ member_id: self.id ]
119
+ end
120
+
121
+ def self.create(*args)
122
+ member = super
123
+ Profile.create( member_id: member.id )
124
+ member
125
+ end
126
+
127
+ def posts( opts = {} )
128
+ limit = opts.fetch(:limit, 30)
129
+ time = Time.at( opts.fetch(:time, Time.now.to_f) ).strftime("%Y-%m-%d %H:%M:%S.%6N%z")
130
+ time_clause = if opts[:newer]
131
+ proc { time_created > time }
132
+ else
133
+ proc { time_created < time }
134
+ end
135
+
136
+ res = Post.where(member_id: self.id).
137
+ where(&time_clause).
138
+ reverse_order(:time_created).
139
+ limit(limit)
140
+
141
+ # optionally restrict to Internet visible posts
142
+ res = res.where(visibility: 'internet') if opts[:public]
143
+ res
144
+ end
145
+
146
+ def comments(n = 10)
147
+ Comment.where(member_id: self.id).reverse_order(:id).limit(n)
148
+ end
149
+
150
+ def pools
151
+ @pools ||= Pool.where( member_id: self.id ).all
152
+ end
153
+ def springs
154
+ @springs ||= Pool.where( member_id: self.id, sprung: true ).all
155
+ end
156
+
157
+ def online?
158
+ self.account && self.account.online?
159
+ end
160
+
161
+ def delete_cascade
162
+ DB.dbh[ "SELECT delete_cascade_member(?)", self.id ].get
163
+ end
164
+
165
+ def dirty
166
+ @account = nil
167
+ @name_display = nil
168
+ @profile = nil
169
+ @pools = nil
170
+ @springs = nil
171
+ end
172
+
173
+ def self.search(name:, include_old: false)
174
+ if include_old
175
+ newness_clause = "TRUE"
176
+ else
177
+ newness_clause = %{
178
+ EXISTS (
179
+ SELECT 1
180
+ FROM posts po
181
+ WHERE
182
+ po.member_id = m.id
183
+ AND po.time_created > NOW() - '30 days'::INTERVAL
184
+ ) OR EXISTS (
185
+ SELECT 1
186
+ FROM comments c
187
+ WHERE
188
+ c.member_id = m.id
189
+ AND c.time_created > NOW() - '30 days'::INTERVAL
190
+ )
191
+ }
192
+ end
193
+
194
+ self.s(
195
+ %{
196
+ SELECT
197
+ m.*
198
+ FROM
199
+ members m
200
+ LEFT OUTER JOIN accounts a ON (a.id = m.account_id)
201
+ LEFT OUTER JOIN profiles p ON (m.id = p.member_id)
202
+ WHERE
203
+ (
204
+ m.username ILIKE '%' || ? || '%'
205
+ OR a.username ILIKE '%' || ? || '%'
206
+ OR p.name_display ILIKE '%' || ? || '%'
207
+ ) AND (
208
+ #{newness_clause}
209
+ )
210
+ },
211
+ name,
212
+ name,
213
+ name
214
+ )
215
+ end
216
+
217
+ def groups
218
+ Libertree::Model::GroupMember.where(member_id: self.id).map { |gm| gm.group }
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,154 @@
1
+ require 'set'
2
+
3
+ module Libertree
4
+ module Model
5
+ class Message < Sequel::Model(:messages)
6
+ include HasDisplayText
7
+
8
+ def sender
9
+ @sender ||= Member[self.sender_member_id]
10
+ end
11
+ alias :member :sender
12
+
13
+ def distribute
14
+ trees = self.recipients.reduce(Set.new) { |_trees, recipient|
15
+ if recipient.tree
16
+ _trees << recipient.tree
17
+ end
18
+ _trees
19
+ }
20
+ recipient_ids = self.recipients.map(&:id)
21
+
22
+ trees.each do |tree|
23
+ Libertree::Model::Job.create(
24
+ {
25
+ task: 'request:MESSAGE',
26
+ params: {
27
+ 'message_id' => self.id,
28
+ 'server_id' => tree.id,
29
+ 'recipient_member_ids' => recipient_ids,
30
+ }.to_json,
31
+ }
32
+ )
33
+ end
34
+ end
35
+
36
+ # forward direct message to the given email address of the provided account
37
+ def forward_via_email(account)
38
+ return unless account
39
+ return unless self.visible_to?(account)
40
+
41
+ Libertree::Model::Job.create(
42
+ task: 'forward-via-email',
43
+ params: {
44
+ 'username' => account.username,
45
+ 'message_id' => self.id
46
+ }.to_json
47
+ )
48
+ end
49
+
50
+ def recipients
51
+ return @recipients if @recipients
52
+ @recipients = Member.s(
53
+ %{
54
+ SELECT
55
+ m.*
56
+ FROM
57
+ members m
58
+ , message_recipients mr
59
+ WHERE
60
+ mr.message_id = ?
61
+ AND m.id = mr.member_id
62
+ },
63
+ self.id
64
+ )
65
+ end
66
+
67
+ # the subset of participants who have not deleted the message
68
+ def active_local_participants
69
+ recipients = Member.s(%{SELECT DISTINCT m.* FROM members m
70
+ JOIN accounts a ON (a.id = m.account_id)
71
+ JOIN message_recipients mr ON (m.id = mr.member_id)
72
+ WHERE mr.message_id = ?
73
+ AND NOT mr.deleted
74
+ }, self.id)
75
+ if ! self.deleted && self.sender.local?
76
+ ( recipients + [self.sender] ).uniq
77
+ else
78
+ recipients
79
+ end
80
+ end
81
+
82
+ def delete_for_participant(local_member)
83
+ # Delete the message for the local participant only, i.e. by
84
+ # removing the assignment. If this is the last local
85
+ # participant, delete the whole message. Note that other
86
+ # recipients / the sender will not see a change in the number
87
+ # of recipients when one of the recipients "deletes" their
88
+ # "copy" of the Message
89
+ participants = active_local_participants
90
+
91
+ # not authorised to delete
92
+ return if participants.empty?
93
+
94
+ if participants == [local_member]
95
+ # This is the only member interested in this message; delete it completely.
96
+ self.delete_cascade
97
+ else
98
+ # there are other local members with a pointer to the
99
+ # message. Only mark as deleted for this recipient.
100
+ DB.dbh[ "UPDATE message_recipients SET deleted=true WHERE message_id = ? AND member_id = ?",
101
+ self.id, local_member.id ].get
102
+ if local_member == self.sender
103
+ self.deleted = true
104
+ self.save
105
+ end
106
+ end
107
+ return true
108
+ end
109
+
110
+ def visible_to?(account)
111
+ self.sender == account.member || recipients.include?(account.member)
112
+ end
113
+
114
+ def self.create_with_recipients(args)
115
+ message = self.create(
116
+ sender_member_id: args[:sender_member_id],
117
+ remote_id: args[:remote_id],
118
+ text: args[:text]
119
+ )
120
+ sender_member = Model::Member[ args[:sender_member_id].to_i ]
121
+
122
+ recipient_member_ids = Array(args[:recipient_member_ids])
123
+ recipient_member_ids.each do |member_id|
124
+ DB.dbh[ "INSERT INTO message_recipients ( message_id, member_id ) VALUES ( ?, ? )", message.id, member_id.to_i ].get
125
+ m = Member[member_id]
126
+ if m.account
127
+ a = m.account
128
+ if ! a.ignoring?(sender_member)
129
+ a.notify_about 'type' => 'message', 'message_id' => message.id
130
+ if a.email && a.settings.forward_dms_via_email
131
+ message.forward_via_email(a)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ message.distribute if sender_member.local?
137
+ message
138
+ end
139
+
140
+ def delete_cascade
141
+ DB.dbh[ "SELECT delete_cascade_message(?)", self.id ].get
142
+ end
143
+
144
+ def to_hash
145
+ {
146
+ 'id' => self.id,
147
+ 'time_created' => self.time_created,
148
+ 'to' => self.recipients.map(&:name_display),
149
+ 'text' => self.text,
150
+ }
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,22 @@
1
+ module Libertree
2
+ module Model
3
+ class Node < Sequel::Model(:nodes)
4
+ ACCESS_MODELS = [ :open,
5
+ :presence,
6
+ :roster,
7
+ :authorize,
8
+ :whitelist ]
9
+ def affiliations
10
+ NodeAffiliation.where(:node_id => self.id)
11
+ end
12
+
13
+ def subs(jid=nil)
14
+ NodeSubscription.for(jid).where(:node_id => self.id)
15
+ end
16
+
17
+ def local_subscribers
18
+ self.subs.join(:accounts, :id => :account_id)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module Libertree
2
+ module Model
3
+ class NodeAffiliation < Sequel::Model(:affiliations)
4
+ TYPES = [ :owner,
5
+ :publisher,
6
+ :'publish-only',
7
+ :member,
8
+ :none,
9
+ :outcast ]
10
+
11
+ many_to_one :node
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ module Libertree
2
+ module Model
3
+ class NodeSubscription < Sequel::Model(:node_subscriptions)
4
+ STATES = [ :none,
5
+ :pending,
6
+ :unconfigured,
7
+ :subscribed ]
8
+
9
+ many_to_one :node
10
+
11
+ def self.for(jid_or_host)
12
+ return self unless jid_or_host
13
+ jid_or_host = jid_or_host.to_s
14
+ if jid_or_host.include?('@')
15
+ self.where(jid: jid_or_host)
16
+ else
17
+ host_pattern = self.where.escape_like(jid_or_host.to_s)
18
+ self.where(Sequel.like(:jid, "%@#{host_pattern}"))
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,71 @@
1
+ require 'json'
2
+
3
+ module Libertree
4
+ module Model
5
+ class Notification < Sequel::Model(:notifications)
6
+ def account
7
+ @account ||= Account[self.account_id]
8
+ end
9
+
10
+ def data
11
+ if val = super
12
+ JSON.parse val
13
+ end
14
+ end
15
+
16
+ def subject
17
+ @subject ||= case self.data['type']
18
+ when 'comment'
19
+ Libertree::Model::Comment[ self.data['comment_id'] ]
20
+ when 'comment-like'
21
+ Libertree::Model::CommentLike[ self.data['comment_like_id'] ]
22
+ when 'message'
23
+ Libertree::Model::Message[ self.data['message_id'] ]
24
+ when 'post-like'
25
+ Libertree::Model::PostLike[ self.data['post_like_id'] ]
26
+ when 'springing'
27
+ Libertree::Model::PoolPost[ self.data['pool_post_id'] ]
28
+ when 'mention', 'group-post'
29
+ Libertree::Model::Post[ self.data['post_id'] ]
30
+ end
31
+ end
32
+
33
+ def self.mark_seen_for_account_and_comment_id(account, comment_ids)
34
+ data = comment_ids.map {|id| %|{"type":"comment","comment_id":#{id.to_i}}| }
35
+ data += CommentLike.where(comment_id: comment_ids).
36
+ map {|like| %|{"type":"comment-like","comment_like_id":#{like.id}}| }
37
+
38
+ self.where(account_id: account.id, data: data).update(seen: true)
39
+ account.dirty
40
+ end
41
+
42
+ def self.mark_seen_for_account_and_post(account, post)
43
+ data = post.likes.map {|like| %|{"type":"post-like","post_like_id":#{like.id.to_i}}| }
44
+ self.where(account_id: account.id, data: data).update(seen: true)
45
+ account.dirty
46
+ end
47
+
48
+ def self.mark_seen_for_account_and_message(account, message)
49
+ self.where("account_id = ? AND data = ?", account.id, %|{"type":"message","message_id":#{message.id}}|).
50
+ update(seen: true)
51
+ account.dirty
52
+ end
53
+
54
+ def self.mark_seen_for_account(account, notification_ids)
55
+ if notification_ids[0] == 'all'
56
+ self.where(account_id: account.id).update(seen: true)
57
+ else
58
+ self.where(account_id: account.id, id: notification_ids).
59
+ update(seen: true)
60
+ end
61
+ account.dirty
62
+ end
63
+
64
+ def self.mark_unseen_for_account(account, notification_ids)
65
+ self.where(account_id: account.id, id: notification_ids).
66
+ update(seen: false)
67
+ account.dirty
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ module Libertree
2
+ module Model
3
+ class PoolPost < Sequel::Model(:pools_posts)
4
+ def pool
5
+ Pool[self.pool_id]
6
+ end
7
+
8
+ def post
9
+ Post[self.post_id]
10
+ end
11
+
12
+ def member
13
+ pool.member
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,172 @@
1
+ module Libertree
2
+ module Model
3
+ class Pool < Sequel::Model(:pools)
4
+ include IsRemoteOrLocal
5
+
6
+ def create_pool_post_job(post)
7
+ Libertree::Model::Job.create_for_forests(
8
+ {
9
+ task: 'request:POOL-POST',
10
+ params: {
11
+ 'pool_id' => self.id,
12
+ 'post_id' => post.id,
13
+ }
14
+ },
15
+ *self.forests
16
+ )
17
+ end
18
+
19
+ def create_pool_delete_job
20
+ Libertree::Model::Job.create_for_forests(
21
+ {
22
+ task: 'request:POOL-DELETE',
23
+ params: { 'pool_id' => self.id, }
24
+ },
25
+ *self.forests
26
+ )
27
+ end
28
+
29
+ def after_create
30
+ super
31
+ if self.local? && self.sprung?
32
+ Libertree::Model::Job.create_for_forests(
33
+ {
34
+ task: 'request:POOL',
35
+ params: { 'pool_id' => self.id, }
36
+ },
37
+ *self.forests
38
+ )
39
+ end
40
+ end
41
+
42
+ def after_update
43
+ super
44
+ if self.local?
45
+ if ! self.sprung?
46
+ self.create_pool_delete_job
47
+ else
48
+ if self.previous_changes.include?(:sprung)
49
+ Libertree::Model::Job.create_for_forests(
50
+ {
51
+ task: 'request:POOL',
52
+ params: { 'pool_id' => self.id, }
53
+ },
54
+ *self.forests
55
+ )
56
+ self.posts.last(16).each do |post|
57
+ self.create_pool_post_job(post)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def before_destroy
65
+ if self.local? && self.sprung?
66
+ self.create_pool_delete_job
67
+ end
68
+ super
69
+ end
70
+
71
+ def member
72
+ @member ||= Member[self.member_id]
73
+ end
74
+
75
+ # TODO: DRY up with member.posts?
76
+ def posts( opts = {} )
77
+ limit = opts.fetch(:limit, 30)
78
+ time = Time.at( opts.fetch(:time, Time.now.to_f) ).strftime("%Y-%m-%d %H:%M:%S.%6N%z")
79
+ time_clause = if opts[:newer]
80
+ proc { time_created > time }
81
+ else
82
+ proc { time_created < time }
83
+ end
84
+
85
+ res = Post.qualify.
86
+ join(:pools_posts, :post_id=>:id).
87
+ where(&time_clause).
88
+ where(:pool_id => self.id).
89
+ reverse_order(:posts__id).
90
+ limit(limit)
91
+
92
+ # optionally restrict to Internet visible posts
93
+ res = res.where(visibility: 'internet') if opts[:public]
94
+ res
95
+ end
96
+
97
+ def includes?(post)
98
+ DB.dbh[ "SELECT EXISTS( SELECT 1 FROM pools_posts WHERE post_id = ? AND pool_id = ? LIMIT 1 )", post.id, self.id ].single_value
99
+ end
100
+
101
+ # NOTE: deletion is NOT distributed
102
+ def delete_cascade
103
+ DB.dbh[ "SELECT delete_cascade_pool(?)", self.id ].get
104
+ end
105
+
106
+ def dirty
107
+ @posts = nil
108
+ self
109
+ end
110
+
111
+ def notify_about_springing(pool_post)
112
+ pool = pool_post.pool
113
+ return if ! pool.sprung
114
+
115
+ post = pool_post.post
116
+ local_post_author = post.member.account
117
+ pool_owner = pool.member.account
118
+
119
+ if(
120
+ local_post_author &&
121
+ local_post_author != pool_owner &&
122
+ ! local_post_author.ignoring?(pool.member)
123
+ )
124
+ local_post_author.notify_about( {
125
+ 'type' => 'springing',
126
+ 'pool_post_id' => pool_post.id,
127
+ } )
128
+ end
129
+ end
130
+
131
+ def <<(post)
132
+ pool_post = PoolPost[ pool_id: self.id, post_id: post.id ]
133
+ if pool_post.nil?
134
+ pool_post_created = true
135
+ pool_post = PoolPost.create(
136
+ 'pool_id' => self.id,
137
+ 'post_id' => post.id
138
+ )
139
+ end
140
+
141
+ self.dirty
142
+ if self.sprung? && pool_post_created
143
+ self.notify_about_springing pool_post
144
+ if self.local?
145
+ create_pool_post_job(post)
146
+ end
147
+ end
148
+ end
149
+
150
+ def remove_post(post)
151
+ DB.dbh[ "DELETE FROM pools_posts WHERE pool_id = ? AND post_id = ?", self.id, post.id ].get
152
+ self.dirty
153
+ if self.local? && self.sprung?
154
+ Libertree::Model::Job.create_for_forests(
155
+ {
156
+ task: 'request:POOL-POST-DELETE',
157
+ params: {
158
+ 'pool_id' => self.id,
159
+ 'post_id' => post.id,
160
+ }
161
+ },
162
+ *self.forests
163
+ )
164
+ end
165
+ end
166
+
167
+ def sprung?
168
+ self.sprung
169
+ end
170
+ end
171
+ end
172
+ end