/* -*- Mode: Pike; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */ //! pingly.pike is a Roxen module that automatically ping Twingly when //! new content is published. //| //| Copyright © 2010 Pontus Östlund (http://www.poppa.se) //| //| License GNU GPL version 3 //| //| pingly.pike is free software: you can redistribute it and/or modify //| it under the terms of the GNU General Public License as published by //| the Free Software Foundation, either version 3 of the License, or //| (at your option) any later version. //| //| pingly.pike is distributed in the hope that it will be useful, //| but WITHOUT ANY WARRANTY; without even the implied warranty of //| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //| GNU General Public License for more details. //| //| You should have received a copy of the GNU General Public License //| along with pingly.pike. If not, see . // {{{ SETTINGS //#define PINGLY_DEBUG #ifdef PINGLY_DEBUG # define TRACE(X...) \ report_debug("Pingly(%s):%d: %s", is_frontend ? "frontend" : "backend", \ __LINE__, sprintf(X)) #else # define TRACE(X...) 0 #endif #include #include inherit "module"; import Sitebuilder; import Sitebuilder.FS; inherit SBConnect; import Parser.XML.Tree; import Protocols.XMLRPC; //LOCALE //DLOCALE #define LOCALE(X,Y) _STR_LOCALE("sitebuilder",X,Y) #define DLOCALE(X,Y) _DEF_LOCALE("sitebuilder",X,Y) #define _ok RXML_CONTEXT->misc[" _ok"] constant thread_safe = 1; constant module_type = MODULE_TAG; LocaleString module_name = DLOCALE(0, "TVAB: Pingly"); /* pingly xml config file www.domain.com /blogg/ My blog /blogg/blog.rss pike,programming,roxen */ LocaleString module_doc = DLOCALE(0, #"

This module listens on the SiteBuilder event hook and should always be connected to a SiteBuilder module.

This module provide functionality for automatically pinging Twingly when new pages are published in a site.

More »

"); // }}} Configuration conf; object hook; private mapping pingly_conf = ([]); private string conf_file; private int(0..1) is_frontend; array(string) valid_content_types; // }}} void create(Configuration cfg) // {{{ { conf = cfg; set_module_creator("Pontus Östlund "); defvar("cfg_path", Variable.File( "", 0, "Config path", "This is the path to the configuration file. This file should be put " "in a Roxen Sitebuilder, thus this should be the virtual path to the " "config file. NOTE! If on a frontend server make sure the config file " "has been replicated." ) ); defvar("valid_content_types", Variable.StringList( ({ "sitebuilder/xml-page-editor" }), 0, "Valid content types", "Only published files with any of these content types will generate a " "ping" ) ); defvar("sitebuilder", ChooseSiteVariable( conf, VAR_INITIAL, DLOCALE(842,"SiteBuilder"), DLOCALE(843,"The SiteBuilder to connect to.") ) ); visible_chooser = CHOOSE_SITE; } // }}} void start(int when, Configuration cfg) // {{{ { module_dependencies(conf, ({ "sitebuilder" })); Site s; if (when == 0 && (s = site())) { is_frontend = s->frontend_mode(); valid_content_types = query("valid_content_types"); if (!sizeof(valid_content_types)) { report_warning("There are no valid content types specified and thus " "this module will have no effect."); } if ((conf_file = query("cfg_path")) && sizeof(conf_file)) { parse_config(); connect_hook(); } else { report_error("No config file path is specified. See the Settings tab!\n"); } } } // }}} void connect_hook() // {{{ { Site s = site(); if (!s) { report_warning(module_name + ": Not connected to CMS Main Module.\n"); hook = 0; return; } hook = s && s->set_event_hooks("pingly", 0, sb_after_hook, 0); } // }}} void sb_after_hook(string operation, string path, RequestID id, void|mapping info, object obj) // {{{ { if (!(operation == "commit" || operation == "replicating")) return; mapping|int cfg; if (!(cfg = is_valid_path(path))) return; // Config file has been updated if (cfg == 1) { parse_config(); return; } string ct = obj->md && obj->md["http-content-type"]; if (search(valid_content_types, ct) == -1) { TRACE("Skipping %s (%s), not an allowed content type!\n", path, ct); return; } TRACE("sb_after_hook(%O, %O, %O)\n", operation, path, cfg); mixed e = catch { switch(operation) { case "commit": case "replicating": if (info && info->commit_type == "create") { TRACE("Will ping in one minute...\n"); // Lets wait a minute... call_out(do_ping, 60, cfg); } } }; if (e) { report_error("Pingly \"hook\" failed: " + describe_error(e) + "\n"); } } // }}} void do_ping(mapping cfg) // {{{ { string rss = cfg["rss-url"]||""; string up = cfg["update-url"]||""; string host = cfg->domain; string url = cfg->url; string name = cfg->name; string kw = cfg->keywords; if (!host || !url || !name) { report_error("Bad configuration section: Missing required node \"domain\" " ",\"url\" or/and \"name\"!\n"); return; } if (search(host, "://") == -1) host = "http://" + host; if (host[-1] == '/') host = host[0..sizeof(host)-2]; if (search(url, "://") == -1) url = host + url; if (sizeof(rss) && search(rss, "://") == -1) rss = host + rss; if (sizeof(up) && search(up, "://") == -1) up = host + up; extended_ping(name, url, up, rss, kw); } // }}} mapping|int is_valid_path(string path) // {{{ { if (path[0] != '/') path = "/" + path; if (path == conf_file) return 1; foreach (indices(pingly_conf), string k) { TRACE("%s ~= %s ? %d\n", k, path, glob(k, path)); if (glob(k, path)) return pingly_conf[k]; } } // }}} void parse_config() // {{{ { TRACE("parse_config(%O)\n", conf_file); Site s = site(); Workarea wa = s->wa_lookup(""); SBObject fobj; catch(fobj = wa && wa->sbobj_va(conf_file, 0)); if (!fobj) { report_error("Couldn't find conf file '%s'", conf_file); return; } if (string xml = get_xml_data(fobj)) { pingly_conf = ([]); Node root = parse_input(xml); foreach (root->get_children(), Node c) if (c->get_node_type() == XML_ELEMENT) { root = c; break; } foreach ((root && root->get_children())||({}), Node child) { if (child->get_node_type() == XML_ELEMENT) { if (child->get_tag_name() == "path") { mapping tmp = ([]); foreach (child->get_children(), Node c) { if (c->get_node_type() != XML_ELEMENT) continue; tmp[c->get_tag_name()] = c->value_of_node(); } if (tmp->url) { string key = tmp->url; if (key[-1] != '/') key = key + "/"; key += "*"; if ( pingly_conf[key] ) { report_error("Duplicate configuration path \"%s\"\n", tmp->url); continue; } pingly_conf[key] = tmp; } } } } TRACE("Config set to: %O\n", pingly_conf); } } // }}} string get_xml_data(object sbobj) // {{{ { string xml; if (objectp(sbobj)) { // Load data from file... // sbobj->view(id, undelete, !id) SBFileData sbfd = sbobj->view(0, 0, 1); if (xml = objectp(sbfd) && sbfd->read()) {} else { report_error("ERROR couldn't load XML from file object: %O\n", sbobj); return 0; } sbfd->close(); } return xml; } // }}} // {{{ Twingly // This is available as a standalone Pike module at: // http://github.com/poppa/Pike-Modules/blob/master/WS.pmod/Twingly.pmod private constant TWINGLY_URL = "http://rpc.twingly.com"; private constant METHOD_PING = "weblogUpdates.ping"; private constant METHOD_EXTENDED_PING = "weblogUpdates.extendedPing"; private int(0..1) print_error = 1; //! Tells Twingly you have new content on your blog. //! //! @seealso //! @[extended_ping], @url{http://rpc.twingly.com/@} //! //! @param blog_name //! @param blog_url //! //! @returns //! @tt{1@} on success, @tt{0@} otherwise. If @[report_fault()] is set //! to @tt{1@} any fault message will be printed to @tt{stderr@}. int(0..1) ping(string blog_name, string blog_url) // {{{ { Client c = Client(TWINGLY_URL); array|Fault res = c[METHOD_PING](safep(blog_name), safep(blog_url)); if (objectp(res)) { if (print_error) { report_error("XML-RPC error (%d): %s\n", res->fault_code, res->fault_string); } return 0; } return 1; } // }}} //! Tells Twingly you have new content on your blog. //! //! @seealso //! @[ping], @url{http://rpc.twingly.com/@} //! //! @param blog_name //! @param blog_url //! @param update_url //! @param rss_url //! @param tags //! //! @returns //! @tt{1@} on success, @tt{0@} otherwise. If @[report_fault()] is set //! to @tt{1@} any fault message will be printed to @tt{stderr@}. int(0..1) extended_ping(string blog_name, string blog_url, string update_url, string rss_url, string tags) // {{{ { Client c = Client(TWINGLY_URL); array|Fault res = c[METHOD_EXTENDED_PING](safep(blog_name), safep(blog_url), safep(update_url), safep(rss_url), safep(tags)); if (objectp(res)) { if (print_error) { report_error("XML-RPC error (%d): %s\n", res->fault_code, res->fault_string); } return 0; } TRACE("PING RESULT: %O\n", res); return 1; } // }}} // Makes sure @[s] isn't null and tries to UTF8-encode it silently. private string safep(string s) // {{{ { if (!s) return ""; catch { return string_to_utf8(s); }; return s; } // }}}