diff --git a/lib/rule.rb b/lib/rule.rb new file mode 100644 index 0000000..8c2432c --- /dev/null +++ b/lib/rule.rb @@ -0,0 +1,107 @@ + + # Similar to an ActiveRecord class, but uses PluginStore for storage instead. Adapted from discourse-data-explorer + # Using this means we can use a standard serializer for sending JSON to the client, and also have convenient save/update/delete methods + # Since this is now being used in two plugins, maybe it should be built into core somehow + class DiscourseChat::Rule + attr_accessor :id, :provider, :channel, :category_id, :tags, :filter + + def initialize(h={}) + h.each {|k,v| public_send("#{k}=",v)} + end + + # saving/loading functions + def self.alloc_id + DistributedMutex.synchronize('discourse-chat_rule-id') do + max_id = DiscourseChat.pstore_get("rule:_id") + max_id = 1 unless max_id + DiscourseChat.pstore_set("rule:_id", max_id + 1) + max_id + end + end + + def self.from_hash(h) + rule = DiscourseChat::Rule.new + [:provider, :channel, :category_id, :tags, :filter].each do |sym| + rule.send("#{sym}=", h[sym]) if h[sym] + end + if h[:id] + rule.id = h[:id].to_i + end + rule + end + + def to_hash + { + id: @id, + provider: @provider, + channel: @channel, + category_id: @category_id, + tags: @tags, + filter: @filter, + } + end + + def self.find(id, opts={}) + hash = DiscourseChat.pstore_get("rule:#{id}") + unless hash + return DiscourseChat::Rule.new if opts[:ignore_deleted] + raise Discourse::NotFound + end + from_hash hash + end + + def save + unless @id && @id > 0 + @id = self.class.alloc_id + end + DiscourseChat.pstore_set "rule:#{id}", to_hash + return self + end + + def destroy + DiscourseChat.pstore_delete "rule:#{id}" + end + + def read_attribute_for_serialization(attr) + self.send(attr) + end + + def self.all_for_provider(provider) + self.where("value::json->>'provider'=?", provider) + end + + def self.all_for_category(category_id) + if category_id.nil? + self.where("json_typeof(value::json->'category_id')='null'") + else + self.where("value::json->>'category_id'=?", category_id.to_s) + end + end + + # Use JSON selectors like this: + # Rule.where("value::json->>'provider'=?", "telegram") + def self.where(*args) + rows = self._all_raw.where(*args) + self._from_psr_rows(rows) + end + + def self.all + self._from_psr_rows(self._all_raw) + end + + def self._all_raw + PluginStoreRow.where(plugin_name: DiscourseChat.plugin_name) + .where("key LIKE 'rule:%'") + .where("key != 'rule:_id'") + end + + def self._from_psr_rows(raw) + raw.map do |psr| + from_hash PluginStore.cast_value(psr.type_name, psr.value) + end + end + + def self.destroy_all + self._all_raw().destroy_all + end + end \ No newline at end of file diff --git a/plugin.rb b/plugin.rb index 387a9ed..7071f2b 100644 --- a/plugin.rb +++ b/plugin.rb @@ -14,10 +14,27 @@ after_initialize do engine_name DiscourseChat::PLUGIN_NAME isolate_namespace DiscourseChat end + + def self.plugin_name + DiscourseChat::PLUGIN_NAME + end + + def self.pstore_get(key) + PluginStore.get(self.plugin_name, key) + end + + def self.pstore_set(key, value) + PluginStore.set(self.plugin_name, key, value) + end + + def self.pstore_delete(key) + PluginStore.remove(self.plugin_name, key) + end end require_relative "lib/provider" require_relative "lib/manager" + require_relative "lib/rule" DiscourseEvent.on(:post_created) do |post| if SiteSetting.chat_enabled? diff --git a/spec/lib/rule_spec.rb b/spec/lib/rule_spec.rb new file mode 100644 index 0000000..ef26426 --- /dev/null +++ b/spec/lib/rule_spec.rb @@ -0,0 +1,101 @@ +require 'rails_helper' + +RSpec.describe DiscourseChat::Rule do + describe '.alloc_id' do + it 'should return sequential numbers' do + expect( DiscourseChat::Rule.alloc_id ).to eq(1) + expect( DiscourseChat::Rule.alloc_id ).to eq(2) + expect( DiscourseChat::Rule.alloc_id ).to eq(3) + end + end + + it 'should save and load successfully' do + expect(DiscourseChat::Rule.all.length).to eq(0) + + rule = DiscourseChat::Rule.new({ + provider:"slack", + channel: "#general", + category_id: 2, + tags: ['hello', 'world'], + filter: 'watch' + }).save + + expect(DiscourseChat::Rule.all.length).to eq(1) + + loadedRule = DiscourseChat::Rule.find(rule.id) + + expect(loadedRule.provider).to eq('slack') + expect(loadedRule.channel).to eq('#general') + expect(loadedRule.category_id).to eq(2) + expect(loadedRule.tags).to contain_exactly('hello','world') + expect(loadedRule.filter).to eq('watch') + + end + + describe 'general operations' do + before do + rule = DiscourseChat::Rule.new({ + provider:"slack", + channel: "#general", + category_id: 2, + tags: ['hello', 'world'] + }).save + end + + it 'can be modified' do + rule = DiscourseChat::Rule.all.first + rule.channel = "#random" + + rule.save + + rule = DiscourseChat::Rule.all.first + expect(rule.channel).to eq('#random') + end + + it 'can be deleted' do + DiscourseChat::Rule.new.save + expect(DiscourseChat::Rule.all.length).to eq(2) + + rule = DiscourseChat::Rule.all.first + rule.destroy + + expect(DiscourseChat::Rule.all.length).to eq(1) + end + + it 'can delete all' do + DiscourseChat::Rule.new.save + DiscourseChat::Rule.new.save + DiscourseChat::Rule.new.save + DiscourseChat::Rule.new.save + + expect(DiscourseChat::Rule.all.length).to eq(5) + + DiscourseChat::Rule.destroy_all + + expect(DiscourseChat::Rule.all.length).to eq(0) + end + + it 'can be filtered by provider' do + rule2 = DiscourseChat::Rule.new({provider:'telegram'}).save + rule3 = DiscourseChat::Rule.new({provider:'slack'}).save + + expect(DiscourseChat::Rule.all.length).to eq(3) + + expect(DiscourseChat::Rule.all_for_provider('slack').length).to eq(2) + expect(DiscourseChat::Rule.all_for_provider('telegram').length).to eq(1) + end + + it 'can be filtered by category' do + rule2 = DiscourseChat::Rule.new({category_id: 1}).save + rule3 = DiscourseChat::Rule.new({category_id: nil}).save + + expect(DiscourseChat::Rule.all.length).to eq(3) + + expect(DiscourseChat::Rule.all_for_category(2).length).to eq(1) + expect(DiscourseChat::Rule.all_for_category(1).length).to eq(1) + expect(DiscourseChat::Rule.all_for_category(nil).length).to eq(1) + end + + end + +end \ No newline at end of file