libertree-model 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
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