forked from gdpelican/babble
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugin.rb
More file actions
447 lines (367 loc) · 12.7 KB
/
plugin.rb
File metadata and controls
447 lines (367 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# name: babble
# about: Shoutbox plugin for Discourse
# version: 1.1.0
# authors: James Kiesel (gdpelican)
# url: https://github.com/gdpelican/babble
register_asset "stylesheets/babble.scss"
BABBLE_PLUGIN_NAME ||= "babble".freeze
enabled_site_setting :babble_enabled
after_initialize do
module ::Babble
class Engine < ::Rails::Engine
engine_name BABBLE_PLUGIN_NAME
isolate_namespace Babble
end
end
Babble::Engine.routes.draw do
get "/topics" => "topics#index"
post "/topics" => "topics#create"
get "/topics/default" => "topics#default"
get "/topics/:id" => "topics#show"
post "/topics/:id" => "topics#update"
delete "/topics/:id" => "topics#destroy"
get "/topics/:id/read/:post_number" => "topics#read"
post "/topics/:id/post" => "topics#create_post"
post "/topics/:id/post/:post_id" => "topics#update_post"
delete "/topics/:id/destroy/:post_id" => "topics#destroy_post"
get "/topics/:id/groups" => "topics#groups"
post "/topics/:id/notification" => "topics#send_notification"
end
Discourse::Application.routes.append do
mount ::Babble::Engine, at: "/babble"
namespace :admin, constraints: StaffConstraint.new do
resources :chats, only: [:show, :index]
end
end
require_dependency "application_controller"
class ::Babble::TopicsController < ::ApplicationController
requires_plugin BABBLE_PLUGIN_NAME
before_filter :ensure_logged_in
before_filter :set_default_id, only: :default
rescue_from('StandardError') { |e| render_json_error e.message, status: 422 }
def index
if current_user.blank?
respond_with_forbidden
else
respond_with Babble::Topic.available_topics_for(current_user), serializer: BasicTopicSerializer
end
end
def show
perform_fetch do
TopicUser.find_or_create_by(user: current_user, topic: topic)
respond_with topic_view, serializer: Babble::TopicViewSerializer
end
end
alias :default :show
def create
perform_update { @topic = Babble::Topic.create_topic(topic_params) }
end
def update
perform_update { Babble::Topic.update_topic(topic, topic_params) }
end
def destroy
if !current_user.admin?
respond_with_forbidden
elsif topic.ordered_posts.any?
Babble::PostDestroyer.new(current_user, topic.ordered_posts.first).destroy
respond_with nil
else
topic.destroy
respond_with nil
end
end
def read
perform_fetch do
topic_user.update(last_read_post_number: params[:post_number]) if topic_user.last_read_post_number.to_i < params[:post_number].to_i
respond_with topic_view, serializer: Babble::TopicViewSerializer
end
end
def create_post
perform_fetch do
@post = Babble::PostCreator.create(current_user, post_creator_params)
if @post.persisted?
success_callback
respond_with @post, serializer: Babble::PostSerializer
else
respond_with_unprocessable
end
end
end
def success_callback
if (SiteSetting.babble_remote_post)
RestClient.post(SiteSetting.babble_remote_url, { current_user: current_user.username, message: @post.cooked }) unless params[:skip_callback]
end
end
def update_post
perform_fetch do
if !guardian.can_edit_post?(topic_post)
respond_with_forbidden
elsif params[:raw].present? && Babble::PostRevisor.new(topic_post, topic).revise!(current_user, params.slice(:raw))
respond_with topic_post, serializer: Babble::PostSerializer
else
respond_with_unprocessable
end
end
end
def destroy_post
perform_fetch do
if !guardian.can_delete_post?(topic_post)
respond_with_forbidden
else
Babble::PostDestroyer.new(current_user, topic_post).destroy
respond_with topic_post, serializer: Babble::PostSerializer
end
end
end
def groups
perform_fetch(require_admin: true) { respond_with topic.allowed_groups, serializer: BasicGroupSerializer }
end
def send_notification
perform_fetch do
Babble::Broadcaster.publish_to_notifications(topic, current_user, notification_params)
respond_with nil
end
end
private
def set_default_id
params[:id] = Babble::Topic.default_topic_for(current_user).try(:id)
end
def perform_fetch(require_admin: false)
if topic.blank?
respond_with_not_found
elsif !current_user.admin && (require_admin || !Babble::Topic.available_topics_for(current_user).include?(topic))
respond_with_forbidden
else
yield
end
end
def perform_update
if !current_user.admin?
respond_with_forbidden
elsif !yield
respond_with_unprocessable
else
respond_with topic_view, serializer: Babble::TopicViewSerializer
end
end
def respond_with(object, serializer: nil)
case object
when Array, ActiveRecord::Relation
render json: ActiveModel::ArraySerializer.new(object, each_serializer: serializer, scope: guardian, root: false).as_json
when nil
render json: { success: :ok }
else
render json: serializer.new(object, scope: guardian, root: false).as_json
end
end
def respond_with_unprocessable
render json: { errors: 'Unable to create or update post' }, status: :unprocessable_entity
end
def respond_with_forbidden
render json: { errors: 'You are unable to view this chat topic' }, status: :forbidden
end
def respond_with_not_found
render json: { errors: 'Chat topic not found' }, status: :not_found
end
def topic
@topic ||= Babble::Topic.find(params[:id])
end
def topic_post
@topic_post ||= topic.posts.find_by(id: params[:post_id])
end
def topic_user
@topic_user ||= TopicUser.find_or_initialize_by(user: current_user, topic: topic)
end
def topic_view
opts = { post_number: topic.highest_post_number } if topic.highest_post_number > 0
@topic_view ||= TopicView.new(topic.id, current_user, opts || {})
end
def topic_params
params.require(:topic).permit(:title, allowed_group_ids: [])
end
def post_creator_params
{
topic_id: params[:id],
raw: params[:raw],
skip_validations: true
}
end
def notification_params
params.require(:state)
end
end
class ::Admin::ChatsController < ::ApplicationController
requires_plugin BABBLE_PLUGIN_NAME
define_method :index, ->{}
define_method :show, ->{}
end
class ::Babble::PostCreator < ::PostCreator
def self.create(user, opts)
Babble::PostCreator.new(user, opts).create
end
def valid?
setup_post
@post.raw.present?
end
def setup_post
super
@topic = @post.topic = Topic.find_by(id: @opts[:topic_id])
end
def update_user_counts
return false
end
def enqueue_jobs
return false
end
def trigger_after_events(post)
super
TopicUser.update_last_read(@user, @topic.id, @post.post_number, PostTiming::MAX_READ_TIME_PER_BATCH)
Babble::Topic.prune_topic(@topic)
Babble::Broadcaster.publish_to_posts(@post, @user)
Babble::Broadcaster.publish_to_topic(@topic, @user)
end
end
class ::Babble::PostRevisor < ::PostRevisor
def revise!(editor, fields, opts={})
opts[:validate_post] = false # don't validate length etc of chat posts
super
end
private
def publish_changes
super
Babble::Broadcaster.publish_to_posts(@post, @editor, is_edit: true)
end
end
class ::Babble::PostDestroyer < ::PostDestroyer
def destroy
super
Babble::Broadcaster.publish_to_topic(@topic, @user)
Babble::Broadcaster.publish_to_posts(@post, @user, is_delete: true)
end
end
class Babble::Broadcaster
def self.publish_to_topic(topic, user, extras = {})
MessageBus.publish "/babble/topics/#{topic.id}", serialized_topic(topic, user, extras)
end
def self.publish_to_posts(post, user, extras = {})
MessageBus.publish "/babble/topics/#{post.topic_id}/posts", serialized_post(post, user, extras)
end
def self.publish_to_notifications(topic, user, status)
MessageBus.publish "/babble/topics/#{topic.id}/notifications", serialized_notification(user, {status: status})
end
def self.serialized_topic(topic, user, extras = {})
serialize(Babble::AnonymousTopicView.new(topic.id, user), user, extras, Babble::TopicViewSerializer)
end
def self.serialized_post(post, user, extras = {})
serialize(post, user, extras, Babble::PostSerializer).as_json.merge(extras)
end
def self.serialized_notification(user, extras = {})
UserSerializer.new(user, scope: Guardian.new).as_json.merge(extras)
end
def self.serialize(obj, user, extras, serializer)
serializer.new(obj, scope: Guardian.new(user), root: false).as_json.merge(extras)
end
end
class ::Babble::Topic
def self.create_topic(params)
return false unless params[:title].present?
save_topic Topic.new, {
user: Discourse.system_user,
category: Babble::Category.instance,
title: params[:title],
visible: false,
allowed_groups: get_allowed_groups(params[:allowed_group_ids])
}
end
def self.update_topic(topic, params)
return false unless params[:title].present?
save_topic topic, {
title: params[:title],
allowed_groups: get_allowed_groups(params[:allowed_group_ids])
}
end
def self.save_topic(topic, params)
topic.tap do |t|
t.assign_attributes(params)
t.save(validate: false) if t.valid? || t.errors.to_hash.except(:title).empty?
end
end
private_class_method :save_topic
def self.get_allowed_groups(ids)
Group.where(id: Array(ids)).presence || default_allowed_groups
end
def self.set_default_allowed_groups(topic)
topic.allowed_group_ids << default_allowed_groups unless topic.allowed_groups.any?
end
def self.default_allowed_groups
Group.find Array Group::AUTO_GROUPS[:trust_level_0]
end
def self.prune_topic(topic)
topic.posts.order(created_at: :desc).offset(SiteSetting.babble_max_topic_size).destroy_all
topic.update(user: Discourse.system_user)
end
def self.default_topic_for(user)
available_topics_for(user).first
end
def self.available_topics
Babble::Category.instance.topics.includes(:allowed_groups)
end
def self.available_topics_for(user)
return Topic.none unless user
available_topics.joins(:allowed_group_users).where("? OR group_users.user_id = ?", user.admin, user.id).uniq
end
# NB: the set_default_allowed_groups block is passed for backwards compatibility,
# so that we never have a topic which has no allowed groups.
def self.find(id)
available_topics.find_by(id: id).tap { |topic| set_default_allowed_groups(topic) if topic }
end
end
# anonymous topic_view for sending out via Message Bus
# (otherwise we end up serializing out the current user's read data to other people)
class ::Babble::AnonymousTopicView < ::TopicView
def topic_user
nil
end
end
class ::Babble::PostSerializer < ::PostSerializer
attributes :image_count
def initialize(object, opts = {})
super object, opts.merge(add_raw: true)
end
end
class ::Babble::TopicViewSerializer < ::TopicViewSerializer
attributes :group_names, :last_posted_at
def group_names
object.topic.allowed_groups.pluck(:name).map(&:humanize)
end
def posts
@posts ||= object.posts.map do |p|
ps = Babble::PostSerializer.new(p, scope: scope, root: false)
ps.topic_view = object
ps.as_json
end
end
def last_read_post_number
super || 0
end
# details are expensive to calculate and we don't use them
def include_details?
false
end
end
class ::Babble::Category
def self.instance
Category.find_by(name: SiteSetting.babble_category_name) ||
Category.new( name: SiteSetting.babble_category_name,
slug: SiteSetting.babble_category_name,
user: Discourse.system_user).tap { |t| t.save(validate: false) }
end
end
class ::Guardian
def can_see_topic?(topic, hide_deleted=true)
super && (is_admin? ||
!Babble::Topic.available_topics.include?(topic) ||
Babble::Topic.available_topics_for(user).include?(topic))
end
end
end