commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / civicrm / tools / bin / scripts / ircbot-civi.py
1 #!/usr/bin/env python
2 '''
3 Push feed and vcs activities to an IRC channel. Configured with the ".slander" rc file, or another yaml file specified on the cmd line.
4
5 CREDITS
6 Miki Tebeka, http://pythonwise.blogspot.com/2009/05/subversion-irc-bot.html
7 Eloff, http://stackoverflow.com/a/925630
8 rewritten by Adam Wight,
9 project homepage is https://github.com/adamwight/slander
10
11 EXAMPLE
12 This is the configuration file used for the CiviCRM project:
13 jobs:
14 svn:
15 changeset_url_format:
16 https://fisheye2.atlassian.com/changelog/CiviCRM?cs=%s
17 root: http://svn.civicrm.org/civicrm
18 args: --username SVN_USER --password SVN_PASSS
19 jira:
20 base_url:
21 http://issues.civicrm.org/jira
22 source:
23 http://issues.civicrm.org/jira/activity?maxResults=20&streams=key+IS+CRM&title=undefined
24
25 irc:
26 host: irc.freenode.net
27 port: 6667
28 nick: civi-activity
29 realname: CiviCRM svn and jira notification bot
30 channel: "#civicrm" #note that quotes are necessary here
31 maxlen: 200
32
33 poll_interval: 60
34
35 sourceURL: https://svn.civicrm.org/tools/trunk/bin/scripts/ircbot-civi.py
36 '''
37
38 import sys
39 import os
40 import re
41
42 import yaml
43
44 from twisted.words.protocols import irc
45 from twisted.internet.protocol import ReconnectingClientFactory
46 from twisted.internet import reactor
47 from twisted.internet.task import LoopingCall
48
49 from xml.etree.cElementTree import parse as xmlparse
50 from cStringIO import StringIO
51 from subprocess import Popen, PIPE
52
53 import feedparser
54 from HTMLParser import HTMLParser
55
56
57 class RelayToIRC(irc.IRCClient):
58 def connectionMade(self):
59 self.config = self.factory.config
60 self.jobs = create_jobs(self.config["jobs"])
61 self.nickname = self.config["irc"]["nick"]
62 self.realname = self.config["irc"]["realname"]
63 self.channel = self.config["irc"]["channel"]
64 self.sourceURL = "https://github.com/adamwight/slander"
65 if "sourceURL" in self.config:
66 self.sourceURL = self.config["sourceURL"]
67
68 irc.IRCClient.connectionMade(self)
69
70 def signedOn(self):
71 self.join(self.channel)
72
73 def joined(self, channel):
74 print "Joined channel %s as %s" % (channel, self.nickname)
75 task = LoopingCall(self.check)
76 task.start(self.config["poll_interval"])
77 print "Started polling jobs, every %d seconds." % (self.config["poll_interval"], )
78
79 def privmsg(self, user, channel, message):
80 if message.find(self.nickname) >= 0:
81 # TODO surely there are useful ways to interact?
82 if re.search(r'\bhelp\b', message):
83 self.say(self.channel, "If I only had a brain: %s" % (self.sourceURL, ))
84 else:
85 print "Failed to handle incoming command: %s said %s" % (user, message)
86
87 def check(self):
88 for job in self.jobs:
89 for line in job.check():
90 self.say(self.channel, str(line))
91 print(line)
92
93 @staticmethod
94 def run(config):
95 factory = ReconnectingClientFactory()
96 factory.protocol = RelayToIRC
97 factory.config = config
98 reactor.connectTCP(config["irc"]["host"], config["irc"]["port"], factory)
99 reactor.run()
100
101 class SvnPoller(object):
102 def __init__(self, root=None, args=None, changeset_url_format=None):
103 self.pre = ["svn", "--xml"] + args.split()
104 self.root = root
105 self.changeset_url_format = changeset_url_format
106 print "Initializing SVN poller: %s" % (" ".join(self.pre)+" "+root, )
107
108 def svn(self, *cmd):
109 pipe = Popen(self.pre + list(cmd) + [self.root], stdout=PIPE)
110 try:
111 data = pipe.communicate()[0]
112 except IOError:
113 data = ""
114 return xmlparse(StringIO(data))
115
116 def revision(self):
117 tree = self.svn("info")
118 revision = tree.find(".//commit").get("revision")
119 return int(revision)
120
121 def revision_info(self, revision):
122 revision = str(revision)
123 tree = self.svn("log", "-r", revision)
124 author = tree.find(".//author").text
125 comment = truncate(strip(tree.find(".//msg").text), self.config["irc"]["maxlen"])
126 url = self.changeset_url(revision)
127
128 return (revision, author, comment, url)
129
130 def changeset_url(self, revision):
131 return self.changeset_url_format % (revision, )
132
133 previous_revision = None
134 def check(self):
135 try:
136 latest = self.revision()
137 if self.previous_revision and latest != self.previous_revision:
138 for rev in range(self.previous_revision + 1, latest + 1):
139 yield "r%s by %s: %s [%s]" % self.revision_info(rev)
140 self.previous_revision = latest
141 except Exception, e:
142 print "ERROR: %s" % e
143
144
145 class FeedPoller(object):
146 last_seen_id = None
147
148 def __init__(self, **config):
149 print "Initializing feed poller: %s" % (config["source"], )
150 self.config = config
151
152 def check(self):
153 result = feedparser.parse(self.config["source"])
154 for entry in result.entries:
155 if (not self.last_seen_id) or (self.last_seen_id == entry.id):
156 break
157 yield self.parse(entry)
158
159 if result.entries:
160 self.last_seen_id = result.entries[0].id
161
162
163 class JiraPoller(FeedPoller):
164 def parse(self, entry):
165 m = re.search(r'(CRM-[0-9]+)$', entry.link)
166 if (not m) or (entry.generator_detail.href != self.config["base_url"]):
167 return
168 issue = m.group(1)
169 summary = truncate(strip(entry.summary), self.config["irc"]["maxlen"])
170 url = self.config["base_url"]+"/browse/%s" % (issue, )
171
172 return "%s: %s %s [%s]" % (entry.author_detail.name, issue, summary, url)
173
174 class MinglePoller(FeedPoller):
175 def parse(self, entry):
176 m = re.search(r'^(.*/([0-9]+))', entry.id)
177 url = m.group(1)
178 issue = int(m.group(2))
179 summary = truncate(strip(entry.summary), self.config["irc"]["maxlen"])
180 author = abbrevs(entry.author_detail.name)
181
182 return "#%d: (%s) %s [%s]" % (issue, author, summary, url)
183
184 def strip(text, html=True, space=True):
185 class MLStripper(HTMLParser):
186 def __init__(self):
187 self.reset()
188 self.fed = []
189 def handle_data(self, d):
190 self.fed.append(d)
191 def get_data(self):
192 return ''.join(self.fed)
193
194 if html:
195 stripper = MLStripper()
196 stripper.feed(text)
197 text = stripper.get_data()
198 if space:
199 text = text.strip().replace("\n", " ")
200 return text
201
202 def abbrevs(name):
203 return "".join([w[:1] for w in name.split()])
204
205 def truncate(message, length):
206 if len(message) > length:
207 return (message[:(length-3)] + "...")
208 else:
209 return message
210
211
212 def create_jobs(d):
213 for type, options in d.items():
214 classname = type.capitalize() + "Poller"
215 klass = globals()[classname]
216 yield klass(**options)
217
218 if __name__ == "__main__":
219 if len(sys.argv) == 2:
220 dotfile = sys.argv[1]
221 else:
222 dotfile = os.path.expanduser("~/.slander")
223 print "Reading config from %s" % (dotfile, )
224 config = yaml.load(file(dotfile))
225 RelayToIRC.run(config)