@@ -22,6 +22,21 @@ module Agents  | 
            ||
| 22 | 22 | 
                * `template` - A JSON object representing a mapping between item output keys and incoming event values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. The `item` key will be repeated for every Event. The `pubDate` key for each item will have the creation time of the Event unless given.  | 
            
| 23 | 23 | 
                * `events_to_show` - The number of events to output in RSS or JSON. (default: `40`)  | 
            
| 24 | 24 | 
                * `ttl` - A value for the <ttl> element in RSS output. (default: `60`)  | 
            
| 25 | 
                +  | 
            |
| 26 | 
                + If you'd like to output RSS tags with attributes, such as `enclosure`, use something like the following in your `template`:  | 
            |
| 27 | 
                +  | 
            |
| 28 | 
                +            "enclosure" => {
               | 
            |
| 29 | 
                +              "_attributes" => {
               | 
            |
| 30 | 
                + "type" => "audio/mpeg",  | 
            |
| 31 | 
                +                "url" => "{{media_url}}"
               | 
            |
| 32 | 
                + }  | 
            |
| 33 | 
                + },  | 
            |
| 34 | 
                +            "tag" => {
               | 
            |
| 35 | 
                +              "_attributes" => {
               | 
            |
| 36 | 
                + "key" => "value"  | 
            |
| 37 | 
                + },  | 
            |
| 38 | 
                + "_contents" => "tag contents (can be an object for nesting)"  | 
            |
| 39 | 
                + }  | 
            |
| 25 | 40 | 
                MD  | 
            
| 26 | 41 | 
                end  | 
            
| 27 | 42 | 
                 | 
            
                @@ -35,15 +50,12 @@ module Agents  | 
            ||
| 35 | 50 | 
                           "item" => {
               | 
            
| 36 | 51 | 
                             "title" => "{{title}}",
               | 
            
| 37 | 52 | 
                             "description" => "Secret hovertext: {{hovertext}}",
               | 
            
| 38 | 
                -            "link" => "{{url}}",
               | 
            |
| 53 | 
                +            "link" => "{{url}}"
               | 
            |
| 39 | 54 | 
                }  | 
            
| 40 | 55 | 
                }  | 
            
| 41 | 56 | 
                }  | 
            
| 42 | 57 | 
                end  | 
            
| 43 | 58 | 
                 | 
            
| 44 | 
                - #"guid" => "",  | 
            |
| 45 | 
                - # "pubDate" => ""  | 
            |
| 46 | 
                -  | 
            |
| 47 | 59 | 
                def working?  | 
            
| 48 | 60 | 
                last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?  | 
            
| 49 | 61 | 
                end  | 
            
                @@ -104,7 +116,7 @@ module Agents  | 
            ||
| 104 | 116 | 
                'title' => feed_title,  | 
            
| 105 | 117 | 
                'description' => feed_description,  | 
            
| 106 | 118 | 
                'pubDate' => Time.now,  | 
            
| 107 | 
                - 'items' => items  | 
            |
| 119 | 
                + 'items' => simplify_item_for_json(items)  | 
            |
| 108 | 120 | 
                }  | 
            
| 109 | 121 | 
                 | 
            
| 110 | 122 | 
                return [content, 200]  | 
            
                @@ -122,7 +134,7 @@ module Agents  | 
            ||
| 122 | 134 | 
                 | 
            
| 123 | 135 | 
                XML  | 
            
| 124 | 136 | 
                 | 
            
| 125 | 
                - content += items.to_xml(:skip_types => true, :root => "items", :skip_instruct => true, :indent => 1).gsub(/^<\/?items>/, '').strip  | 
            |
| 137 | 
                + content += simplify_item_for_xml(items).to_xml(skip_types: true, root: "items", skip_instruct: true, indent: 1).gsub(/^<\/?items>/, '').strip  | 
            |
| 126 | 138 | 
                 | 
            
| 127 | 139 | 
                content += Utils.unindent(<<-XML)  | 
            
| 128 | 140 | 
                </channel>  | 
            
                @@ -139,5 +151,71 @@ module Agents  | 
            ||
| 139 | 151 | 
                end  | 
            
| 140 | 152 | 
                end  | 
            
| 141 | 153 | 
                end  | 
            
| 154 | 
                +  | 
            |
| 155 | 
                + private  | 
            |
| 156 | 
                +  | 
            |
| 157 | 
                + class XMLNode  | 
            |
| 158 | 
                + def initialize(tag_name, attributes, contents)  | 
            |
| 159 | 
                + @tag_name, @attributes, @contents = tag_name, attributes, contents  | 
            |
| 160 | 
                + end  | 
            |
| 161 | 
                +  | 
            |
| 162 | 
                + def to_xml(options)  | 
            |
| 163 | 
                + if @contents.is_a?(Hash)  | 
            |
| 164 | 
                + options[:builder].tag! @tag_name, @attributes do  | 
            |
| 165 | 
                +            @contents.each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options.merge(skip_instruct: true)) }
               | 
            |
| 166 | 
                + end  | 
            |
| 167 | 
                + else  | 
            |
| 168 | 
                + options[:builder].tag! @tag_name, @attributes, @contents  | 
            |
| 169 | 
                + end  | 
            |
| 170 | 
                + end  | 
            |
| 171 | 
                + end  | 
            |
| 172 | 
                +  | 
            |
| 173 | 
                + def simplify_item_for_xml(item)  | 
            |
| 174 | 
                + if item.is_a?(Hash)  | 
            |
| 175 | 
                +        item.each.with_object({}) do |(key, value), memo|
               | 
            |
| 176 | 
                + if value.is_a?(Hash)  | 
            |
| 177 | 
                +            if value.key?('_attributes') || value.key?('_contents')
               | 
            |
| 178 | 
                + memo[key] = XMLNode.new(key, value['_attributes'], simplify_item_for_xml(value['_contents']))  | 
            |
| 179 | 
                + else  | 
            |
| 180 | 
                + memo[key] = simplify_item_for_xml(value)  | 
            |
| 181 | 
                + end  | 
            |
| 182 | 
                + else  | 
            |
| 183 | 
                + memo[key] = value  | 
            |
| 184 | 
                + end  | 
            |
| 185 | 
                + end  | 
            |
| 186 | 
                + elsif item.is_a?(Array)  | 
            |
| 187 | 
                +        item.map { |value| simplify_item_for_xml(value) }
               | 
            |
| 188 | 
                + else  | 
            |
| 189 | 
                + item  | 
            |
| 190 | 
                + end  | 
            |
| 191 | 
                + end  | 
            |
| 192 | 
                +  | 
            |
| 193 | 
                + def simplify_item_for_json(item)  | 
            |
| 194 | 
                + if item.is_a?(Hash)  | 
            |
| 195 | 
                +        item.each.with_object({}) do |(key, value), memo|
               | 
            |
| 196 | 
                + if value.is_a?(Hash)  | 
            |
| 197 | 
                +            if value.key?('_attributes') || value.key?('_contents')
               | 
            |
| 198 | 
                + contents = if value['_contents'] && value['_contents'].is_a?(Hash)  | 
            |
| 199 | 
                + simplify_item_for_json(value['_contents'])  | 
            |
| 200 | 
                + elsif value['_contents']  | 
            |
| 201 | 
                +                           { "contents" => value['_contents'] }
               | 
            |
| 202 | 
                + else  | 
            |
| 203 | 
                +                           {}
               | 
            |
| 204 | 
                + end  | 
            |
| 205 | 
                +  | 
            |
| 206 | 
                +              memo[key] = contents.merge(value['_attributes'] || {})
               | 
            |
| 207 | 
                + else  | 
            |
| 208 | 
                + memo[key] = simplify_item_for_json(value)  | 
            |
| 209 | 
                + end  | 
            |
| 210 | 
                + else  | 
            |
| 211 | 
                + memo[key] = value  | 
            |
| 212 | 
                + end  | 
            |
| 213 | 
                + end  | 
            |
| 214 | 
                + elsif item.is_a?(Array)  | 
            |
| 215 | 
                +        item.map { |value| simplify_item_for_json(value) }
               | 
            |
| 216 | 
                + else  | 
            |
| 217 | 
                + item  | 
            |
| 218 | 
                + end  | 
            |
| 219 | 
                + end  | 
            |
| 142 | 220 | 
                end  | 
            
| 143 | 221 | 
                end  | 
            
                @@ -53,7 +53,9 @@ module Agents  | 
            ||
| 53 | 53 | 
                end  | 
            
| 54 | 54 | 
                end  | 
            
| 55 | 55 | 
                end  | 
            
| 56 | 
                - private  | 
            |
| 56 | 
                +  | 
            |
| 57 | 
                + private  | 
            |
| 58 | 
                +  | 
            |
| 57 | 59 | 
                def request_guard(&blk)  | 
            
| 58 | 60 | 
                response = yield  | 
            
| 59 | 61 | 
                       error("Error during http request: #{response.body}") if response.code > 400
               | 
            
                @@ -83,7 +83,7 @@ describe Agents::DataOutputAgent do  | 
            ||
| 83 | 83 | 
                expect(status).to eq(200)  | 
            
| 84 | 84 | 
                end  | 
            
| 85 | 85 | 
                 | 
            
| 86 | 
                - describe "returning events as RSS and JSON" do  | 
            |
| 86 | 
                + describe "outputtng events as RSS and JSON" do  | 
            |
| 87 | 87 | 
                let!(:event1) do  | 
            
| 88 | 88 | 
                         agents(:bob_website_agent).create_event :payload => {
               | 
            
| 89 | 89 | 
                "url" => "http://imgs.xkcd.com/comics/evolving.png",  | 
            
                @@ -194,5 +194,136 @@ describe Agents::DataOutputAgent do  | 
            ||
| 194 | 194 | 
                })  | 
            
| 195 | 195 | 
                end  | 
            
| 196 | 196 | 
                end  | 
            
| 197 | 
                +  | 
            |
| 198 | 
                + describe "outputting nesting" do  | 
            |
| 199 | 
                + before do  | 
            |
| 200 | 
                +        agent.options['template']['item']['enclosure'] = {
               | 
            |
| 201 | 
                +          "_attributes" => {
               | 
            |
| 202 | 
                + "type" => "audio/mpeg",  | 
            |
| 203 | 
                +            "url" => "{{media_url}}"
               | 
            |
| 204 | 
                + },  | 
            |
| 205 | 
                + "_self_closing" => "true"  | 
            |
| 206 | 
                + }  | 
            |
| 207 | 
                +        agent.options['template']['item']['foo'] = {
               | 
            |
| 208 | 
                +          "_attributes" => {
               | 
            |
| 209 | 
                +            "attr" => "attr-value-{{foo}}"
               | 
            |
| 210 | 
                + },  | 
            |
| 211 | 
                +          "_contents" => "Foo: {{foo}}"
               | 
            |
| 212 | 
                + }  | 
            |
| 213 | 
                +        agent.options['template']['item']['nested'] = {
               | 
            |
| 214 | 
                +          "_attributes" => {
               | 
            |
| 215 | 
                + "key" => "value"  | 
            |
| 216 | 
                + },  | 
            |
| 217 | 
                +          "_contents" => {
               | 
            |
| 218 | 
                + "title" => "some title"  | 
            |
| 219 | 
                + }  | 
            |
| 220 | 
                + }  | 
            |
| 221 | 
                +        agent.options['template']['item']['simpleNested'] = {
               | 
            |
| 222 | 
                + "title" => "some title",  | 
            |
| 223 | 
                +          "complex" => {
               | 
            |
| 224 | 
                +            "_attributes" => {
               | 
            |
| 225 | 
                + "key" => "value"  | 
            |
| 226 | 
                + },  | 
            |
| 227 | 
                +            "_contents" => {
               | 
            |
| 228 | 
                +              "first" => {
               | 
            |
| 229 | 
                +                "_attributes" => {
               | 
            |
| 230 | 
                + "a" => "b"  | 
            |
| 231 | 
                + },  | 
            |
| 232 | 
                +                "_contents" => {
               | 
            |
| 233 | 
                + "second" => "value"  | 
            |
| 234 | 
                + }  | 
            |
| 235 | 
                + }  | 
            |
| 236 | 
                + }  | 
            |
| 237 | 
                + }  | 
            |
| 238 | 
                + }  | 
            |
| 239 | 
                + agent.save!  | 
            |
| 240 | 
                + end  | 
            |
| 241 | 
                +  | 
            |
| 242 | 
                + let!(:event) do  | 
            |
| 243 | 
                +        agents(:bob_website_agent).create_event :payload => {
               | 
            |
| 244 | 
                + "url" => "http://imgs.xkcd.com/comics/evolving.png",  | 
            |
| 245 | 
                + "title" => "Evolving",  | 
            |
| 246 | 
                + "hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.",  | 
            |
| 247 | 
                + "media_url" => "http://google.com/audio.mpeg",  | 
            |
| 248 | 
                + "foo" => 1  | 
            |
| 249 | 
                + }  | 
            |
| 250 | 
                + end  | 
            |
| 251 | 
                +  | 
            |
| 252 | 
                + it "can output JSON" do  | 
            |
| 253 | 
                +        content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
               | 
            |
| 254 | 
                + expect(status).to eq(200)  | 
            |
| 255 | 
                + expect(content['items'].first).to eq(  | 
            |
| 256 | 
                +          {
               | 
            |
| 257 | 
                + 'title' => 'Evolving',  | 
            |
| 258 | 
                + 'description' => 'Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.',  | 
            |
| 259 | 
                + 'link' => 'http://imgs.xkcd.com/comics/evolving.png',  | 
            |
| 260 | 
                + 'guid' => event.id,  | 
            |
| 261 | 
                + 'pubDate' => event.created_at.rfc2822,  | 
            |
| 262 | 
                +            'enclosure' => {
               | 
            |
| 263 | 
                + "type" => "audio/mpeg",  | 
            |
| 264 | 
                + "url" => "http://google.com/audio.mpeg"  | 
            |
| 265 | 
                + },  | 
            |
| 266 | 
                +            'foo' => {
               | 
            |
| 267 | 
                + 'attr' => 'attr-value-1',  | 
            |
| 268 | 
                + 'contents' => 'Foo: 1'  | 
            |
| 269 | 
                + },  | 
            |
| 270 | 
                +            'nested' => {
               | 
            |
| 271 | 
                + "key" => "value",  | 
            |
| 272 | 
                + "title" => "some title"  | 
            |
| 273 | 
                + },  | 
            |
| 274 | 
                +            'simpleNested' => {
               | 
            |
| 275 | 
                + "title" => "some title",  | 
            |
| 276 | 
                +              "complex" => {
               | 
            |
| 277 | 
                + "key"=>"value",  | 
            |
| 278 | 
                +                "first" => {
               | 
            |
| 279 | 
                + "a" => "b",  | 
            |
| 280 | 
                + "second"=>"value"  | 
            |
| 281 | 
                + }  | 
            |
| 282 | 
                + }  | 
            |
| 283 | 
                + }  | 
            |
| 284 | 
                + }  | 
            |
| 285 | 
                + )  | 
            |
| 286 | 
                + end  | 
            |
| 287 | 
                +  | 
            |
| 288 | 
                + it "can output RSS" do  | 
            |
| 289 | 
                +        stub(agent).feed_link { "https://yoursite.com" }
               | 
            |
| 290 | 
                +        content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
               | 
            |
| 291 | 
                + expect(status).to eq(200)  | 
            |
| 292 | 
                +        expect(content_type).to eq('text/xml')
               | 
            |
| 293 | 
                + expect(content.gsub(/\s+/, '')).to eq Utils.unindent(<<-XML).gsub(/\s+/, '')  | 
            |
| 294 | 
                + <?xml version="1.0" encoding="UTF-8" ?>  | 
            |
| 295 | 
                + <rss version="2.0">  | 
            |
| 296 | 
                + <channel>  | 
            |
| 297 | 
                + <title>XKCD comics as a feed</title>  | 
            |
| 298 | 
                + <description>This is a feed of recent XKCD comics, generated by Huginn</description>  | 
            |
| 299 | 
                + <link>https://yoursite.com</link>  | 
            |
| 300 | 
                +           <lastBuildDate>#{Time.now.rfc2822}</lastBuildDate>
               | 
            |
| 301 | 
                +           <pubDate>#{Time.now.rfc2822}</pubDate>
               | 
            |
| 302 | 
                + <ttl>60</ttl>  | 
            |
| 303 | 
                +  | 
            |
| 304 | 
                + <item>  | 
            |
| 305 | 
                + <title>Evolving</title>  | 
            |
| 306 | 
                + <description>Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.</description>  | 
            |
| 307 | 
                + <link>http://imgs.xkcd.com/comics/evolving.png</link>  | 
            |
| 308 | 
                +             <pubDate>#{event.created_at.rfc2822}</pubDate>
               | 
            |
| 309 | 
                + <enclosure type="audio/mpeg" url="http://google.com/audio.mpeg" />  | 
            |
| 310 | 
                + <foo attr="attr-value-1">Foo: 1</foo>  | 
            |
| 311 | 
                + <nested key="value"><title>some title</title></nested>  | 
            |
| 312 | 
                + <simpleNested>  | 
            |
| 313 | 
                + <title>some title</title>  | 
            |
| 314 | 
                + <complex key="value">  | 
            |
| 315 | 
                + <first a="b">  | 
            |
| 316 | 
                + <second>value</second>  | 
            |
| 317 | 
                + </first>  | 
            |
| 318 | 
                + </complex>  | 
            |
| 319 | 
                + </simpleNested>  | 
            |
| 320 | 
                +             <guid>#{event.id}</guid>
               | 
            |
| 321 | 
                + </item>  | 
            |
| 322 | 
                +  | 
            |
| 323 | 
                + </channel>  | 
            |
| 324 | 
                + </rss>  | 
            |
| 325 | 
                + XML  | 
            |
| 326 | 
                + end  | 
            |
| 327 | 
                + end  | 
            |
| 197 | 328 | 
                end  | 
            
| 198 | 329 | 
                end  | 
            
                @@ -54,8 +54,9 @@ describe Agents::WunderlistAgent do  | 
            ||
| 54 | 54 | 
                 | 
            
| 55 | 55 | 
                describe "#receive" do  | 
            
| 56 | 56 | 
                it "send a message to the hipchat" do  | 
            
| 57 | 
                -      stub_request(:post, 'https://a.wunderlist.com/api/v1/tasks').with { |request| request.body == 'abc'}
               | 
            |
| 57 | 
                + stub_request(:post, 'https://a.wunderlist.com/api/v1/tasks')  | 
            |
| 58 | 58 | 
                @checker.receive([@event])  | 
            
| 59 | 
                + expect(WebMock).to have_requested(:post, "https://a.wunderlist.com/api/v1/tasks")  | 
            |
| 59 | 60 | 
                end  | 
            
| 60 | 61 | 
                end  | 
            
| 61 | 62 | 
                 |