├── .gitignore ├── settings.py ├── LICENSE ├── README.markdown └── ircamp.py /.gitignore: -------------------------------------------------------------------------------- 1 | local_settings.py 2 | *.pyc -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # create a local_settings.py and overwrite 2 | # these values: 3 | 4 | BLESSED_USER = "your_name" # the bot only listens to one user. 5 | BOT_NAME = "ircamp" 6 | IRC_CHANNEL = "private_room" 7 | CAMPFIRE_SUBDOMAIN = "mycompany" 8 | CAMPFIRE_ROOM = "The Good Room" 9 | CAMPFIRE_EMAIL = "ircamp@gmail.com" 10 | CAMPFIRE_PASSWORD = "1rc4mp" 11 | 12 | 13 | 14 | 15 | # you probably don't want anything below 16 | # this line in local_settings.py 17 | 18 | IRC_SERVER = "irc.freenode.net" 19 | IRC_PORT = 6667 20 | 21 | try: 22 | from local_settings import * 23 | except ImportError: 24 | pass 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | IRCamp: IRC <-> Campfire Bridge 2 | =============================== 3 | 4 | IRCamp allows you to use Campfire from the comfort of your favorite IRC 5 | client by acting as a thin bridge between the two services. 6 | 7 | Right now only the basic bridge is completed. Check the [issues][1] 8 | for future plans. 9 | 10 | Requirements 11 | ------------ 12 | 13 | * Python 2.3+ 14 | * Pinder >= 0.6.5a (defunkt's fork) 15 | * BeautifulSoup >= 3.0.4 16 | * httplib2 >= 0.3.0 17 | * Twisted >= 2.5.0 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | $ easy_install -U BeautifulSoup 24 | $ easy_install -U httplib2 25 | $ git clone git://github.com/defunkt/pinder.git 26 | $ cd pinder && python setup.py install 27 | $ git clone git://github.com/defunkt/ircamp.git 28 | $ cd ircamp 29 | 30 | 31 | Configuration 32 | ------------- 33 | 34 | You'll want to create a local_settings.py inside your new `ircamp` 35 | directory. It should look something like this: 36 | 37 | BLESSED_USER = "your_name" 38 | IRC_CHANNEL = "private_room" 39 | CAMPFIRE_SUBDOMAIN = "mycompany" 40 | CAMPFIRE_ROOM = "The Good Room" 41 | CAMPFIRE_EMAIL = "ircamp@gmail.com" 42 | CAMPFIRE_PASSWORD = "1rc4mp" 43 | 44 | The bot will only respond to `BLESSED_USER`, so ensure it's a registered 45 | irc nickname. 46 | 47 | 48 | Usage 49 | ----- 50 | 51 | $ python ircamp.py 52 | 53 | 54 | Bugs! Features! 55 | --------------- 56 | 57 | Please add them to [the ircamp issues][1]. 58 | 59 | Thanks. 60 | 61 | Chris Wanstrath // chris@ozmm.org 62 | 63 | [1]: http://github.com/defunkt/ircamp/issues 64 | -------------------------------------------------------------------------------- /ircamp.py: -------------------------------------------------------------------------------- 1 | # sys 2 | import re 3 | from htmlentitydefs import name2codepoint as n2cp 4 | from datetime import datetime 5 | 6 | # twisted 7 | from twisted.words.protocols import irc 8 | from twisted.internet import reactor, protocol, task 9 | from twisted.python import log 10 | 11 | # pinder 12 | import pinder 13 | 14 | # BeautifulSoup 15 | from BeautifulSoup import BeautifulSoup 16 | 17 | # config 18 | from settings import * 19 | 20 | class CampfireBot(object): 21 | """The Campfire part of the IRC <-> Campfire bridge.""" 22 | 23 | def __init__(self, subdomain, room, email, password): 24 | self.host = "http://%s.campfirenow.com" % subdomain 25 | self.subdomain = subdomain 26 | self.email = email 27 | self.client = pinder.Campfire(subdomain) 28 | self.client.login(email, password) 29 | self.room = self.client.find_room_by_name(room) 30 | self.room.join() 31 | 32 | def __str__(self): 33 | return "<%s: %s as %s>" % (self.host, self.room, self.email) 34 | 35 | def __getattr__(self, name): 36 | return getattr(self.room, name) 37 | 38 | def logout(self): 39 | self.room.leave() 40 | self.client.logout() 41 | 42 | def todays_transcript_url(self): 43 | path = '/room/%s/transcript/%s' % (self.id, 44 | datetime.now().strftime('%Y/%m/%d')) 45 | return self.host + path 46 | 47 | 48 | # message filters 49 | 50 | class MessageFilter(object): 51 | def __init__(self, message): 52 | self.message = message 53 | 54 | @classmethod 55 | def filter_message(cls, message): 56 | for subclass in cls.__subclasses__(): 57 | message = subclass(message).filter() 58 | return message 59 | 60 | def filter(self): 61 | return self.message 62 | 63 | 64 | class IRCMessageFilter(MessageFilter): 65 | pass 66 | 67 | 68 | class TwitterFilter(IRCMessageFilter): 69 | def filter(self): 70 | if 'twitter.com/' in self.message: 71 | id = re.search(r'(\d+)', self.message).group(0) 72 | self.message = 'http://twictur.es/i/%s.gif' % id 73 | return self.message 74 | 75 | 76 | class CampfireMessageFilter(MessageFilter): 77 | def __init__(self, message): 78 | self.message = message 79 | self.soup = BeautifulSoup(message['message'].decode('unicode_escape')) 80 | 81 | 82 | class ActionFilter(CampfireMessageFilter): 83 | def filter(self): 84 | if re.search(r'has (entered|left) the room', self.message['message']): 85 | pass 86 | elif re.search(r'^\*(.+)\*$', self.message['message']): 87 | self.message['message'] = self.message['message'].replace('*', '') 88 | else: 89 | self.message['person'] = self.message['person'] + ':' 90 | 91 | return self.message 92 | 93 | 94 | class PasteFilter(CampfireMessageFilter): 95 | def filter(self): 96 | paste = self.soup.find('pre') 97 | if paste: 98 | url = self.soup.find('a')['href'] 99 | # hax 100 | host = "http://%s.campfirenow.com" % CAMPFIRE_SUBDOMAIN 101 | self.message['message'] = host + url 102 | return self.message 103 | 104 | 105 | class ImageFilter(CampfireMessageFilter): 106 | def filter(self): 107 | image = self.soup.find('img') 108 | 109 | if image: 110 | url = str(image['src']) 111 | if "twictur.es" in url: 112 | url = self.twicture_url(url) 113 | self.message['message'] = url 114 | 115 | return self.message 116 | 117 | def twicture_url(self, image): 118 | return image.replace('/i/', '/r/').replace('.gif', '') 119 | 120 | 121 | class LinkFilter(CampfireMessageFilter): 122 | def filter(self): 123 | link = self.soup.find('a') 124 | if link and len(self.soup.findAll(True)) == 1: 125 | self.message['message'] = str(link['href']) 126 | return self.message 127 | 128 | 129 | class IRCBot(irc.IRCClient): 130 | """The IRC part of the IRC <-> Campfire bridge.""" 131 | 132 | nickname = BOT_NAME 133 | 134 | # twisted callbacks 135 | 136 | def connectionMade(self): 137 | irc.IRCClient.connectionMade(self) 138 | self.campfire = CampfireBot(self.factory.subdomain, self.factory.room, 139 | self.factory.email, self.factory.password) 140 | self.channel = '#%s' % self.factory.channel 141 | self.lc = task.LoopingCall(self.new_messages_from_campfire) 142 | self.lc.start(5, False) 143 | 144 | def connectionLost(self, reason): 145 | irc.IRCClient.connectionLost(self, reason) 146 | self.campfire.logout() 147 | 148 | def new_messages_from_campfire(self): 149 | self.campfire.ping() 150 | try: 151 | for message in self.campfire.messages(): 152 | message = CampfireMessageFilter.filter_message(message) 153 | msg = "%s %s" % (message['person'], message['message']) 154 | msg = self.decode_htmlentities(msg.decode('unicode_escape')) 155 | self.speak(msg) 156 | except socket.timeout: 157 | pass 158 | 159 | # irc callbacks 160 | 161 | def signedOn(self): 162 | self.join(self.channel) 163 | self.commands = IRCCommands(campfire=self.campfire, irc=self) 164 | 165 | def joined(self, channel): 166 | self.speak("Room '%s' in %s: %s" % 167 | (self.factory.room, self.factory.subdomain, 168 | self.campfire.todays_transcript_url())) 169 | 170 | def irc_PING(self, prefix, params): 171 | irc.IRCClient.irc_PING(self, prefix, params) 172 | self.campfire.ping() 173 | 174 | def action(self, user, channel, data): 175 | user = user.split('!')[0] 176 | action = '*' + data + '*' 177 | 178 | if user == BLESSED_USER: 179 | self.campfire.speak(action) 180 | 181 | self.log(channel, user, action) 182 | 183 | 184 | def privmsg(self, user, channel, msg): 185 | user = user.split('!')[0] 186 | self.log(channel, user, msg) 187 | 188 | if user == BLESSED_USER: 189 | if self.iscommand(msg): 190 | parts = msg.split(' ') 191 | command = parts[1] 192 | args = parts[2:] 193 | out = self.commands._send(command, args) 194 | self.speak(out) 195 | else: 196 | out = IRCMessageFilter.filter_message(msg) 197 | self.campfire.speak(out) 198 | 199 | def iscommand(self, msg): 200 | return BOT_NAME in msg.split(' ')[0] 201 | 202 | # other bot methods 203 | 204 | def speak(self, message): 205 | self.msg(self.channel, str(message)) 206 | self.log(self.channel, self.nickname, message) 207 | 208 | def log(self, channel, user, msg): 209 | print "%s <%s> %s" % (channel, user, msg) 210 | 211 | def __str__(self): 212 | return "<%s: %s as %s>" % (IRC_SERVER, self.channel, self.nickname) 213 | 214 | def decode_htmlentities(self, string): 215 | """ 216 | Decode HTML entities-hex, decimal, or named-in a string 217 | @see http://snippets.dzone.com/posts/show/4569 218 | @see http://github.com/sku/python-twitter-ircbot/blob/321d94e0e40d0acc92f5bf57d126b57369da70de/html_decode.py 219 | """ 220 | def substitute_entity(match): 221 | ent = match.group(3) 222 | if match.group(1) == "#": 223 | # decoding by number 224 | if match.group(2) == '': 225 | # number is in decimal 226 | return unichr(int(ent)) 227 | elif match.group(2) == 'x': 228 | # number is in hex 229 | return unichr(int('0x'+ent, 16)) 230 | else: 231 | # they were using a name 232 | cp = n2cp.get(ent) 233 | if cp: return unichr(cp) 234 | else: return match.group() 235 | 236 | entity_re = re.compile(r'&(#?)(x?)(\w+);') 237 | return entity_re.subn(substitute_entity, string)[0] 238 | 239 | 240 | class IRCBotFactory(protocol.ClientFactory): 241 | """ 242 | A factory for IRCBot. 243 | 244 | A new protocol instance will be created each time we connect to the server. 245 | """ 246 | 247 | protocol = IRCBot 248 | 249 | def __init__(self): 250 | self.channel = IRC_CHANNEL 251 | self.subdomain = CAMPFIRE_SUBDOMAIN 252 | self.room = CAMPFIRE_ROOM 253 | self.email = CAMPFIRE_EMAIL 254 | self.password = CAMPFIRE_PASSWORD 255 | 256 | def clientConnectionLost(self, connector, reason): 257 | """Reconnect to server on disconnect.""" 258 | connector.connect() 259 | 260 | def clientConnectionFailed(self, connector, reason): 261 | print "connection failed:", reason 262 | reactor.stop() 263 | 264 | class IRCCommands(object): 265 | """ 266 | Commands the IRC bot responds to. 267 | 268 | Each method is a command, passed all subsequent words. 269 | 270 | e.g. 271 | 272 | bot: help 273 | calls: bot.help([]) 274 | 275 | bot: guest on 276 | calls: bot.guest(['on']) 277 | 278 | Returning a non-empty string replies to the channel. 279 | """ 280 | def __init__(self, campfire, irc): 281 | self.campfire = campfire 282 | self.irc = irc 283 | 284 | def _send(self, command, args): 285 | """Dispatch method. Not a command.""" 286 | try: 287 | method = getattr(self, command) 288 | return method(args) 289 | except: 290 | return '' 291 | 292 | def help(self, args): 293 | methods = dir(self) 294 | methods.remove('_send') 295 | methods = [x for x in methods if not '__' in x and type(getattr(self, x)) == type(self._send)] 296 | return "I know these commands: " + ', '.join(methods) 297 | 298 | def users(self, args): 299 | return ', '.join(self.campfire.users()) 300 | 301 | def transcript(self, args): 302 | return self.campfire.todays_transcript_url() 303 | 304 | 305 | if __name__ == '__main__': 306 | f = IRCBotFactory() 307 | reactor.connectTCP(IRC_SERVER, IRC_PORT, f) 308 | reactor.run() 309 | --------------------------------------------------------------------------------