Skip to content
This repository was archived by the owner on Aug 4, 2020. It is now read-only.

Commit c1e676b

Browse files
committed
split further fix a huge bug where reversing the feed would print only old items
1 parent fee3434 commit c1e676b

File tree

9 files changed

+204
-148
lines changed

9 files changed

+204
-148
lines changed

brain.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import re
2+
3+
from job import JobQueue
4+
5+
class Brain(object):
6+
def __init__(self, config, sink=None):
7+
self.config = config
8+
self.sink = sink
9+
self.project_url = "https://github.com/adamwight/slander"
10+
if "project_url" in self.config:
11+
self.project_url = self.config["project_url"]
12+
13+
def say(self, message):
14+
self.sink.say(self.sink.channel, message)
15+
16+
def respond(self, user, message):
17+
if re.search(r'\bhelp\b', message):
18+
self.say("If I only had a brain: %s -- Commands: help jobs kill last" % (self.project_url, ))
19+
elif re.search(r'\bjobs\b', message):
20+
jobs_desc = JobQueue.describe()
21+
jobs_desc = re.sub(r'p(ass)?w(ord)?[ :=]*[^ ]+', r'p***word', jobs_desc)
22+
23+
self.say("Running jobs [%s]" % (jobs_desc, ))
24+
#elif re.search(r'\bkill\b', message):
25+
# self.say("Squeal! Killed by %s" % (user, ))
26+
# self.factory.stopTrying()
27+
# self.quit()
28+
elif re.search(r'\blast\b', message):
29+
if self.sink.timestamp:
30+
self.say("My last post was %s UTC" % (self.sink.timestamp, ))
31+
else:
32+
self.say("No activity.")
33+
else:
34+
print "Failed to handle incoming command: %s said %s" % (user, message)

feed.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import feedparser
2-
from HTMLParser import HTMLParser
32

43
class FeedPoller(object):
54
"""
@@ -14,14 +13,15 @@ def __init__(self, source=None):
1413
def check(self):
1514
result = feedparser.parse(self.source)
1615
result.entries.reverse()
16+
skipping = True
1717
for entry in result.entries:
18-
if (not self.last_seen_id) or (self.last_seen_id == entry.id):
19-
if not test:
20-
break
21-
yield self.parse(entry)
18+
if (self.last_seen_id == entry.id):
19+
skipping = False
20+
elif not skipping:
21+
yield self.parse(entry)
2222

2323
if result.entries:
24-
self.last_seen_id = result.entries[0].id
24+
self.last_seen_id = result.entries[-1].id
2525

2626
def parse(self, entry):
2727
return "%s [%s]" % (entry.summary, entry.link)

irc.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import datetime
2+
3+
#pip install twisted twisted.words
4+
from twisted.words.protocols import irc
5+
from twisted.internet.protocol import ReconnectingClientFactory
6+
from twisted.internet import reactor
7+
8+
import text
9+
from job import JobQueue
10+
from brain import Brain
11+
12+
class RelayToIRC(irc.IRCClient):
13+
"""
14+
Bot brain will spawn listening jobs and then relay results to an irc channel.
15+
"""
16+
timestamp = None
17+
18+
def connectionMade(self):
19+
self.config = self.factory.config
20+
self.nickname = self.config["irc"]["nick"]
21+
self.realname = self.config["irc"]["realname"]
22+
self.channel = self.config["irc"]["channel"]
23+
if "maxlen" in self.config["irc"]:
24+
text.maxlen = self.config["irc"]["maxlen"]
25+
26+
irc.IRCClient.connectionMade(self)
27+
28+
def signedOn(self):
29+
self.join(self.channel)
30+
31+
def joined(self, channel):
32+
print "Joined channel %s as %s" % (channel, self.nickname)
33+
#XXX get outta here:
34+
source = JobQueue(self.config["jobs"], self, self.config["poll_interval"])
35+
source.run()
36+
self.brain = Brain(self.config, sink=self)
37+
38+
def privmsg(self, user, channel, message):
39+
if message.find(self.nickname) >= 0:
40+
self.brain.respond(user, message)
41+
42+
def write(self, data):
43+
if isinstance(data, list):
44+
for line in data:
45+
self.write(line)
46+
return
47+
self.say(self.channel, str(data))
48+
self.timestamp = datetime.datetime.utcnow()
49+
50+
51+
@staticmethod
52+
def run(config):
53+
factory = ReconnectingClientFactory()
54+
factory.protocol = RelayToIRC
55+
factory.config = config
56+
reactor.connectTCP(config["irc"]["host"], config["irc"]["port"], factory)
57+
reactor.run()

jira.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import text
2+
from feed import FeedPoller
3+
4+
import re
5+
16
class JiraPoller(FeedPoller):
27
"""
38
Polls a Jira RSS feed and formats changes to issue trackers.
@@ -11,7 +16,7 @@ def parse(self, entry):
1116
if (not m) or (entry.generator_detail.href != self.base_url):
1217
return
1318
issue = m.group(1)
14-
summary = truncate(strip(entry.summary))
19+
summary = text.strip(entry.summary, truncate=True)
1520
url = "%s/browse/%s" % (self.base_url, issue)
1621

1722
return "%s: %s %s -- %s" % (entry.author_detail.name, issue, summary, url)

job.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from twisted.internet.task import LoopingCall
2+
3+
jobs = []
4+
5+
class JobQueue(object):
6+
def __init__(self, definition, sink, interval):
7+
"""
8+
Read job definitions from a config source, create an instance of the job using its configuration, and store the config for reference.
9+
"""
10+
self.sink = sink
11+
self.interval = interval
12+
for type_name, options in definition.items():
13+
classname = type_name.capitalize() + "Poller"
14+
m = __import__(type_name, fromlist=[classname])
15+
if hasattr(m, classname):
16+
klass = getattr(m, classname)
17+
job = klass(**options)
18+
job.config = options
19+
job.config['class'] = type_name
20+
jobs.append(job)
21+
else:
22+
raise Exception("Failed to create job of type " + classname)
23+
24+
@staticmethod
25+
def describe():
26+
jobs_desc = ", ".join(
27+
[("%s: %s" % (j.config['class'], j.config))
28+
for j in jobs]
29+
)
30+
return jobs_desc
31+
32+
def check(self):
33+
for job in jobs:
34+
for line in job.check():
35+
if line:
36+
self.sink.write(line)
37+
38+
def run(self):
39+
task = LoopingCall(self.check)
40+
task.start(self.interval)
41+
print "Started polling jobs, every %d seconds." % (self.interval, )

mingle.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import text
2+
from feed import FeedPoller
3+
4+
import re
5+
16
class MinglePoller(FeedPoller):
27
"""
38
Format changes to Mingle cards
@@ -6,7 +11,7 @@ def parse(self, entry):
611
m = re.search(r'^(.*/([0-9]+))', entry.id)
712
url = m.group(1)
813
issue = int(m.group(2))
9-
author = abbrevs(entry.author_detail.name)
14+
author = text.abbrevs(entry.author_detail.name)
1015

1116
assignments = []
1217
details = entry.content[0].value
@@ -22,18 +27,21 @@ def parse(self, entry):
2227
elif re.match(r'Planning - Sprint', m.group('property')):
2328
n = re.search(r'(Sprint \d+)', m.group('value'))
2429
normal_form = "->" + n.group(1)
30+
elif 'Deployed' == m.group('value'):
31+
normal_form = "*Deployed*"
2532
else:
26-
normal_form = abbrevs(m.group('property')+" : "+m.group('value'))
33+
normal_form = text.abbrevs(m.group('property')+" : "+m.group('value'))
2734

2835
if normal_form:
2936
assignments.append(normal_form)
3037
summary = '|'.join(assignments)
3138

32-
# TODO 'Description changed'
3339
for m in re.finditer(r'(?P<property>[^:>]+): (?P<value>[^<]+)', details):
3440
if m.group('property') == 'Comment added':
3541
summary = m.group('value')+" "+summary
42+
for m in re.finditer(r'Description changed', details):
43+
summary += " " + m.group(0)
3644

37-
summary = truncate(summary)
45+
summary = text.trunc(summary)
3846

3947
return "#%d: (%s) %s -- %s" % (issue, author, summary, url)

slander.py

Lines changed: 9 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -32,150 +32,18 @@
3232
3333
poll_interval: 60
3434
35-
sourceURL: https://svn.civicrm.org/tools/trunk/bin/scripts/ircbot-civi.py
35+
project_url: https://svn.civicrm.org/tools/trunk/bin/scripts/ircbot-civi.py
3636
'''
3737

38+
from irc import RelayToIRC
39+
from job import JobQueue
40+
3841
import sys
3942
import os
40-
import re
41-
import datetime
4243

4344
# pip install pyyaml
4445
import yaml
4546

46-
#pip install twisted twisted.words
47-
from twisted.words.protocols import irc
48-
from twisted.internet.protocol import ReconnectingClientFactory
49-
from twisted.internet import reactor
50-
from twisted.internet.task import LoopingCall
51-
52-
test = False
53-
maxlen = 200
54-
config = None
55-
56-
class RelayToIRC(irc.IRCClient):
57-
"""
58-
Bot brain will spawn listening jobs and then relay results to an irc channel.
59-
TODO:
60-
* separate polling manager from irc protocol
61-
* operator ACL and commands which perform an action
62-
"""
63-
sourceURL = "https://github.com/adamwight/slander"
64-
timestamp = None
65-
66-
def connectionMade(self):
67-
self.config = self.factory.config
68-
self.jobs = create_jobs(self.config["jobs"])
69-
self.nickname = self.config["irc"]["nick"]
70-
self.realname = self.config["irc"]["realname"]
71-
self.channel = self.config["irc"]["channel"]
72-
global maxlen
73-
if "maxlen" in self.config["irc"]:
74-
maxlen = self.config["irc"]["maxlen"]
75-
if "sourceURL" in self.config:
76-
self.sourceURL = self.config["sourceURL"]
77-
78-
irc.IRCClient.connectionMade(self)
79-
80-
def signedOn(self):
81-
self.join(self.channel)
82-
83-
def joined(self, channel):
84-
print "Joined channel %s as %s" % (channel, self.nickname)
85-
task = LoopingCall(self.check)
86-
task.start(self.config["poll_interval"])
87-
print "Started polling jobs, every %d seconds." % (self.config["poll_interval"], )
88-
89-
def privmsg(self, user, channel, message):
90-
if message.find(self.nickname) >= 0:
91-
if re.search(r'\bhelp\b', message):
92-
self.say(channel, "If I only had a brain: %s -- Commands: help jobs kill last" % (self.sourceURL, ))
93-
elif re.search(r'\bjobs\b', message):
94-
jobs_desc = ", ".join(
95-
[("%s: %s" % (j.config['class'], j.config))
96-
for j in self.jobs]
97-
)
98-
jobs_desc = re.sub(r'p(ass)?w(ord)?[ :=]*[^ ]+', r'p***word', jobs_desc)
99-
100-
self.say(channel, "Running jobs [%s]" % (jobs_desc, ))
101-
#elif re.search(r'\bkill\b', message):
102-
# self.say(self.channel, "Squeal! Killed by %s" % (user, ))
103-
# self.factory.stopTrying()
104-
# self.quit()
105-
elif re.search(r'\blast\b', message):
106-
if self.timestamp:
107-
self.say(channel, "My last post was %s UTC" % (self.timestamp, ))
108-
else:
109-
self.say(channel, "No activity.")
110-
else:
111-
print "Failed to handle incoming command: %s said %s" % (user, message)
112-
113-
def check(self):
114-
for job in self.jobs:
115-
for line in job.check():
116-
if line:
117-
if test:
118-
print(line)
119-
self.say(self.channel, str(line))
120-
self.timestamp = datetime.datetime.utcnow()
121-
122-
@staticmethod
123-
def run(config):
124-
factory = ReconnectingClientFactory()
125-
factory.protocol = RelayToIRC
126-
factory.config = config
127-
reactor.connectTCP(config["irc"]["host"], config["irc"]["port"], factory)
128-
reactor.run()
129-
130-
131-
def strip(text, html=True, space=True):
132-
class MLStripper(HTMLParser):
133-
def __init__(self):
134-
self.reset()
135-
self.fed = []
136-
def handle_data(self, d):
137-
self.fed.append(d)
138-
def get_data(self):
139-
return ''.join(self.fed)
140-
141-
if html:
142-
stripper = MLStripper()
143-
stripper.feed(text)
144-
text = stripper.get_data()
145-
if space:
146-
text = re.sub("\s+", " ", text).strip()
147-
return text
148-
149-
def abbrevs(name):
150-
"""
151-
Turn a space-delimited name into initials, e.g. Frank Ubiquitous Zappa -> FUZ
152-
"""
153-
return "".join([w[:1] for w in name.split()])
154-
155-
def truncate(message):
156-
if len(message) > maxlen:
157-
return (message[:(maxlen-3)] + "...")
158-
else:
159-
return message
160-
161-
162-
def create_jobs(d):
163-
"""
164-
Read job definitions from a config source, create an instance of the job using its configuration, and store the config for reference.
165-
"""
166-
jobs = []
167-
for type_name, options in d.items():
168-
m = __import__(type_name)
169-
classname = type_name.capitalize() + "Poller"
170-
if hasattr(m, classname):
171-
klass = getattr(m, classname)
172-
job = klass(**options)
173-
job.config = options
174-
job.config['class'] = type_name
175-
jobs.append(job)
176-
else:
177-
sys.exit("Failed to create job of type " + classname)
178-
return jobs
17947

18048
def load_config(path):
18149
dotfile = os.path.expanduser(path)
@@ -203,6 +71,11 @@ def parse_args(args):
20371
if not config:
20472
sys.exit(args[0] + ": No config!")
20573

74+
global test
75+
test = False
76+
if "test" in config:
77+
test = config["test"]
78+
20679
return config
20780

20881
if __name__ == "__main__":

0 commit comments

Comments
 (0)