├── .github └── FUNDING.yml ├── .gitignore ├── CHANGES ├── COPYING ├── README.md ├── doc └── wookie.png └── wookie ├── __init__.py ├── config.py └── wookie.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [barnumbirr] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################################################################# 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | # it's better to unpack these files and commit the raw source 12 | # git has its own built in compression methods 13 | ################################################################# 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ################################################################# 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ################################################################# 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | Icon? 37 | ehthumbs.db 38 | Thumbs.db 39 | *.pyc 40 | 41 | # Atom.io projects # 42 | ################################################################# 43 | /.project 44 | 45 | # Sublime Text projects # 46 | ################################################################# 47 | *.sublime-* 48 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Version 3.2 2 | ============= 3 | February 2016 4 | 5 | * fixed README, LICENSE 6 | 7 | January 2016 8 | 9 | * add blacklist entries 10 | * add easy irc colors 11 | * all pubmsg logged to irc-entries 12 | * logs moved to /home/USERNAME/.wookie_logs 13 | * fix API issues [.get] 14 | 15 | Version 3.0 16 | ============= 17 | 18 | December 2014 19 | 20 | * complete code refactor, move to ircbot (grm34) 21 | 22 | Version 2.2 23 | ============= 24 | 25 | October 2014 26 | 27 | * JOIN in irc.add_global_handler (grm34) 28 | * bot config in external file 29 | 30 | Version 2.1 31 | ============= 32 | 33 | April 2014 34 | 35 | * re-added CTCP capability that somehow got lost during push to v.2.0 36 | 37 | Version 2.0 38 | ============= 39 | 40 | April 2014 41 | 42 | * added command support 43 | * fixed ugly ass indentation 44 | * rename project from iRC-RSSbot to wookie 45 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Apache v2.0 License 2 | Copyright 2012-2016 Martin Simon, Jérémy Pardo 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

wookiewookie

2 | 3 | **wookie** is a simple, open source, easy-to-use iRC RSS bot written in Python. 4 | It was designed to announce new torrent releases in a given iRC channel. 5 | 6 | ## Features 7 | 8 | * Easy configuration 9 | * No heavy database required 10 | * SSL support 11 | * Support for additional commands 12 | * Blacklist entries 13 | * IRC Colors 14 | * API Search 15 | 16 | ## Required: 17 | 18 | ``` 19 | $ apt-get install python-pip 20 | $ pip install feedparser django 21 | $ wget http://sourceforge.net/projects/python-irclib/files/python-irclib/0.4.8/python-irclib-0.4.8.tar.gz 22 | $ tar -zxvf python-irclib-0.4.8.tar.gz && rm python-irclib-0.4.8.tar.gz && cd python-irclib-0.4.8 23 | $ python setup.py install 24 | ``` 25 | 26 | ## Installation: 27 | 28 | ``` 29 | $ cd /home/ 30 | $ git clone https://github.com/mrsmn/wookie.git 31 | $ cd wookie/wookie 32 | ``` 33 | 34 | Edit the config.py file to suit your needs, then: 35 | 36 | ``` 37 | $ python wookie.py or 38 | ``` 39 | 40 | ## Awesome contributors: 41 | 42 | * [grm34](https://github.com/grm34) 43 | 44 | ## License 45 | 46 | ``` 47 | Apache v2.0 License 48 | Copyright 2012-2016 Martin Simon, Jérémy Pardo 49 | 50 | Licensed under the Apache License, Version 2.0 (the "License"); 51 | you may not use this file except in compliance with the License. 52 | You may obtain a copy of the License at 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | Unless required by applicable law or agreed to in writing, software 57 | distributed under the License is distributed on an "AS IS" BASIS, 58 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 59 | See the License for the specific language governing permissions and 60 | limitations under the License. 61 | 62 | ``` 63 | 64 | ## Buy me a coffee? 65 | 66 | If you feel like buying me a coffee (or a beer?), donations are welcome: 67 | 68 | ``` 69 | WDC : WbcWJzVD8yXt3yLnnkCZtwQo4YgSUdELkj 70 | HBN : F2Zs4igv8r4oJJzh4sh4bGmeqoUxLQHPki 71 | DOGE: DRBkryyau5CMxpBzVmrBAjK6dVdMZSBsuS 72 | ``` 73 | 74 | All credit for the wookie icon goes to James Hance. 75 | -------------------------------------------------------------------------------- /doc/wookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barnumbirr/wookie/8acab04e31a420e3c74c7b73a8e9e5b07fd1717f/doc/wookie.png -------------------------------------------------------------------------------- /wookie/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __title__ = 'wookie' 5 | __version__ = '3.2' 6 | __authors__ = 'Martin Simon , Jérémy Pardo ' 7 | __repo__ = 'https://github.com/mrsmn/wookie' 8 | __license__ = 'Apache v2.0 License' 9 | -------------------------------------------------------------------------------- /wookie/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | wookie = { 5 | 'bot_owner': [''], 6 | 'start_bot': 'screen -dmS wookie', 7 | 'kill_bot': 'screen -X -S wookie kill', 8 | 'mode': 'standard' 9 | } 10 | 11 | network = { 12 | 'server': '', 13 | 'port': 6667, 14 | 'SSL': False, 15 | 'ipv6': False, 16 | 'channels': [''], 17 | 'bot_nick': 'wookie', 18 | 'bot_name': 'wookie v.3.2 is available at ' 19 | 'https://github.com/mrsmn/wookie', 20 | 'password': '' 21 | } 22 | 23 | feeds = { 24 | 'queue_delay': .5, 25 | 'announce_delay': 5.0, 26 | 'request_delay': 5.0, 27 | 'announce': [''], 28 | 'request': [''] 29 | } 30 | 31 | api = { 32 | 'api_url': '', 33 | 'authkey': '' 34 | } 35 | 36 | blacklist = { 37 | 'announce': ['test1', 'test2', 'test3'], 38 | 'request': ['test1', 'test2', 'test3'] 39 | } 40 | -------------------------------------------------------------------------------- /wookie/wookie.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import ssl 7 | import json 8 | import time 9 | import irclib 10 | import socket 11 | import urllib2 12 | import calendar 13 | import commands 14 | import optparse 15 | import threading 16 | import feedparser 17 | from json import loads 18 | from irclib import SimpleIRCClient 19 | from threading import (Thread, Event) 20 | from datetime import (datetime, timedelta) 21 | from django.utils.encoding import smart_str 22 | from urllib2 import (urlopen, URLError, HTTPError) 23 | from config import (feeds, wookie, network, api, blacklist) 24 | 25 | class Queue_Manager(Thread): 26 | 27 | def __init__(self, connection, delay=feeds['queue_delay']): 28 | Thread.__init__(self) 29 | self.setDaemon(1) 30 | self.connection = connection 31 | self.delay = delay 32 | self.event = Event() 33 | self.queue = [] 34 | 35 | def run(self): 36 | while 1: 37 | self.event.wait() 38 | while self.queue: 39 | (msg, target) = self.queue.pop(0) 40 | self.connection.privmsg(target, msg) 41 | time.sleep(self.delay) 42 | self.event.clear() 43 | 44 | def send(self, msg, target): 45 | self.queue.append((msg.strip(), target)) 46 | self.event.set() 47 | 48 | 49 | class _wookie(SimpleIRCClient): 50 | 51 | def __init__(self): 52 | irclib.SimpleIRCClient.__init__(self) 53 | self.start_time = time.time() 54 | self.queue = Queue_Manager(self.connection) 55 | 56 | self.BLACK = '\x0301' 57 | self.BLUE = '\x0302' 58 | self.RED = '\x0304' 59 | self.YELLOW = '\x0308' 60 | self.GREEN = '\x0303' 61 | self.PURPLE = '\x0306' 62 | self.PINK = '\x0313' 63 | self.ORANGE = '\x0307' 64 | self.TEAL = '\x0310' 65 | self.BOLD = '\x02' 66 | self.ITALIC = '\x1D' 67 | self.UNDERLINE = '\x1F' 68 | self.SWAP = '\x16' 69 | self.END = '\x0F' 70 | 71 | def on_welcome(self, serv, ev): 72 | if network['password']: 73 | serv.privmsg( 74 | "nickserv", "IDENTIFY {}".format(network['password'])) 75 | serv.privmsg("chanserv", "SET irc_auto_rejoin ON") 76 | serv.privmsg("chanserv", "SET irc_join_delay 0") 77 | for channel in network['channels']: 78 | serv.join(channel) 79 | try: 80 | self.history_manager() 81 | self.announce_refresh() 82 | self.request_refresh() 83 | time.sleep(5) 84 | self.queue.start() 85 | except (OSError, IOError) as error: 86 | serv.disconnect() 87 | print(error) 88 | sys.exit(1) 89 | 90 | def on_rss_entry(self, text): 91 | for channel in network['channels']: 92 | self.queue.send(text, channel) 93 | 94 | def on_kick(self, serv, ev): 95 | serv.join(ev.target()) 96 | 97 | def on_invite(self, serv, ev): 98 | serv.join(ev.arguments()[0]) 99 | 100 | def on_ctcp(self, serv, ev): 101 | if ev.arguments()[0].upper() == 'VERSION': 102 | serv.ctcp_reply( 103 | ev.source().split('!')[0], network['bot_name']) 104 | 105 | def history_manager(self): 106 | home = '{}/.wookie_logs'.format(os.environ.get('HOME')) 107 | self.wookie_path = os.path.dirname(os.path.realpath(__file__)) 108 | self.announce_entries = '{}/announce-entries'.format(home) 109 | self.request_entries = '{}/request-entries'.format(home) 110 | self.irc_entries = '{}/irc-entries'.format(home) 111 | if os.path.exists(home) is False: 112 | os.system('mkdir {}'.format(home)) 113 | if os.path.exists(self.announce_entries) is False: 114 | os.system('touch {}'.format(self.announce_entries)) 115 | if os.path.exists(self.request_entries) is False: 116 | os.system('touch {}'.format(self.request_entries)) 117 | if os.path.exists(self.irc_entries) is False: 118 | os.system('touch {}'.format(self.irc_entries)) 119 | 120 | def restart_bot(self, serv, ev): 121 | serv.disconnect() 122 | if wookie['mode'] == 'screen': 123 | current_screen = self.get_current_screen() 124 | os.system('{0} {1}/./wookie.py run && screen -X -S {2} kill' 125 | .format(wookie['start_bot'], self.wookie_path, 126 | current_screen)) 127 | else: 128 | os.system('{}/./wookie.py start'.format(self.wookie_path)) 129 | sys.exit(1) 130 | 131 | def get_current_screen(self): 132 | screen_list = commands.getoutput('screen -list') 133 | screen_lines = smart_str( 134 | screen_list.replace('\t', '')).splitlines() 135 | for screen in screen_lines: 136 | if 'wookie' in screen: 137 | current_screen = screen.split('.')[0] 138 | return current_screen 139 | 140 | def timestamp(self, date): 141 | return calendar.timegm(date.timetuple()) 142 | 143 | def get_nice_size(self, num, suffix='B'): 144 | for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: 145 | if abs(num) < 1024.0: 146 | return "%3.1f%s%s" % (num, unit, suffix) 147 | num /= 1024.0 148 | return "%.1f%s%s" % (num, 'Yi', suffix) 149 | 150 | def get_rls_pretime(self, pre): 151 | (years, remainder) = divmod(pre, 31556926) 152 | (days, remainder1) = divmod(remainder, 86400) 153 | (hours, remainder2) = divmod(remainder1, 3600) 154 | (minutes, seconds) = divmod(remainder2, 60) 155 | if pre < 60: 156 | pretime = '{}secs after Pre'.format(seconds) 157 | elif pre < 3600: 158 | pretime = '{0}min {1}secs after Pre'.format(minutes, seconds) 159 | elif pre < 86400: 160 | pretime = '{0}h {1}min after Pre'.format(hours, minutes) 161 | elif pre < 172800: 162 | pretime = '{0}jour {1}h after Pre'.format(days, hours) 163 | elif pre < 31556926: 164 | pretime = '{0}jours {1}h after Pre'.format(days, hours) 165 | elif pre < 63113852: 166 | pretime = '{0}an {1}jours after Pre'.format(years, days) 167 | else: 168 | pretime = '{0}ans {1}jours after Pre'.format(years, days) 169 | return pretime 170 | 171 | def search_release(self, serv, ev, message, chan): 172 | data = loads(urlopen('{0}{1}{2}{3}{4}{5}'.format( 173 | api['api_url'], 'torrent/search&ak=', 174 | api['authkey'], '&q=', 175 | smart_str(message[5:].replace(' ', '+').replace('.', '+')), 176 | '&nb=1'), None, 5.0).read()) 177 | 178 | id = smart_str(data[0]['id']) 179 | title = smart_str(data[0]['name']).replace(' ', '.') 180 | url = '{0}{1}{2}/{3}'.format( 181 | api['api_url'].replace('api/', ''), 182 | 'torrent/', id, title) 183 | completed = smart_str(data[0]['times_completed']) 184 | leechers = smart_str(data[0]['leechers']) 185 | seeders = smart_str(data[0]['seeders']) 186 | added = smart_str(data[0]['added']) 187 | comments = smart_str(data[0]['comments']) 188 | size = self.get_nice_size(int(data[0]['size'])) 189 | predate = smart_str(data[0]['pretime']) 190 | pretime = '' 191 | if predate != '0': 192 | releaseDate = datetime.strptime( 193 | added, '%Y-%m-%d %H:%M:%S') 194 | pre = (self.timestamp(releaseDate)-(int(predate)+3600)) 195 | pretime = ' | {0}Pretime:{1} {2}'.format( 196 | self.BOLD, self.END, self.get_rls_pretime(int(pre))) 197 | 198 | serv.privmsg(chan, '{0}{1}:{2} {3}'.format( 199 | self.BOLD, title, self.END, url)) 200 | serv.privmsg( 201 | chan, '{7}Added on:{8} {0}{1} | {7}Size:{8} {2} ' 202 | '| {7}Seeders:{8} {3} | {7}Leechers:{8} {4} ' 203 | '| {7}Completed:{8} {5} | {7}Comments:{8} {6}' 204 | .format(added, pretime, size, seeders, 205 | leechers, completed, comments, self.BOLD, self.END)) 206 | 207 | def on_privmsg(self, serv, ev): 208 | author = irclib.nm_to_n(ev.source()) 209 | message = ev.arguments()[0].strip() 210 | arguments = message.split(' ') 211 | if author in wookie['bot_owner']: 212 | if '.say' == arguments[0] and len(arguments) > 2: 213 | serv.privmsg( 214 | arguments[1], message.replace(arguments[0], '') 215 | .replace(arguments[1], '')[2:]) 216 | if '.act' == arguments[0] and len(arguments) > 2: 217 | serv.action( 218 | arguments[1], message.replace(arguments[0], '') 219 | .replace(arguments[1], '')[2:]) 220 | if '.join' == arguments[0] and len(arguments) > 2: 221 | serv.join(message[3:]) 222 | if '.part' == arguments[0] and len(arguments) > 2: 223 | serv.part(message[3:]) 224 | 225 | def on_pubmsg(self, serv, ev): 226 | author = irclib.nm_to_n(ev.source()) 227 | message = ev.arguments()[0].strip() 228 | arguments = message.split(' ') 229 | event_time = time.strftime('[%H:%M:%S]', time.localtime()) 230 | record = '{0} {1}: {2}'.format(event_time, author, message) 231 | FILE = open(self.irc_entries, "a") 232 | FILE.write("{}\n".format(record)) 233 | FILE.close() 234 | print (record) 235 | chan = ev.target() 236 | if author in wookie['bot_owner']: 237 | try: 238 | if ev.arguments()[0].lower() == '.restart': 239 | self.restart_bot(serv, ev) 240 | if ev.arguments()[0].lower() == '.quit': 241 | serv.disconnect() 242 | if not wookie['mode']: 243 | os.system(wookie['kill_bot']) 244 | sys.exit(1) 245 | except OSError as error: 246 | serv.disconnect() 247 | print(error) 248 | sys.exit(1) 249 | 250 | if '.help' == arguments[0].lower(): 251 | serv.privmsg( 252 | chan, '{0}{2}{3}Available commands:{1}{0} .help || ' 253 | '.version || .uptime || .restart || .quit || ' 254 | '.get {1}'.format( 255 | self.BOLD, self.END, self.UNDERLINE, self.BLUE)) 256 | 257 | if '.version' == arguments[0].lower(): 258 | serv.privmsg(chan, '{0}{1}{2}{3}'.format( 259 | self.BOLD, self.PINK, network['bot_name'], self.END)) 260 | 261 | if '.uptime' == arguments[0].lower(): 262 | uptime_raw = round(time.time() - self.start_time) 263 | uptime = timedelta(seconds=uptime_raw) 264 | serv.privmsg(chan, '{0}{3}[UPTIME]{4} {2}{1}'.format( 265 | self.BOLD, self.END, uptime, self.TEAL, self.BLACK)) 266 | 267 | if '.get' == arguments[0].lower() and len(arguments) > 1: 268 | try: 269 | self.search_release(serv, ev, message, chan) 270 | except (HTTPError, URLError, KeyError, 271 | ValueError, TypeError, AttributeError): 272 | serv.privmsg( 273 | chan, '{0}Nothing found, sorry about this.{1}'.format( 274 | self.BOLD, self.END)) 275 | pass 276 | except socket.timeout: 277 | serv.privmsg( 278 | chan, "{0}{1}[ERROR]{2} API timeout...".format( 279 | self.BOLD, self.RED, self.END)) 280 | pass 281 | 282 | def announce_refresh(self): 283 | FILE = open(self.announce_entries, "r") 284 | filetext = FILE.read() 285 | FILE.close() 286 | 287 | for feed in feeds['announce']: 288 | d = feedparser.parse(feed) 289 | for entry in d.entries: 290 | id_announce = '{0}{1}'.format(smart_str(entry.link), 291 | smart_str(entry.title)) 292 | if id_announce not in filetext and\ 293 | any([x not in id_announce for x in blacklist['announce']]): 294 | url = smart_str(entry.link) 295 | title = smart_str( 296 | entry.title).split(' - ', 1)[1].replace(' ', '.') 297 | size = smart_str( 298 | entry.description).split('|')[1].replace( 299 | 'Size :', '').strip() 300 | category = smart_str(entry.title).split(' -', 1)[0]\ 301 | .replace(' ', '-') 302 | if len(entry.description.split('|')) == 5: 303 | pretime = '' 304 | else: 305 | releaseDate = datetime.strptime(smart_str( 306 | entry.description).split('|')[2].replace( 307 | smart_str('Ajouté le :'), '').strip(), 308 | '%Y-%m-%d %H:%M:%S') 309 | preDate = datetime.strptime(smart_str( 310 | entry.description).split('|')[5].replace( 311 | 'PreTime :', '').strip(), 312 | '%Y-%m-%d %H:%M:%S') 313 | pre = ( 314 | self.timestamp(releaseDate)-self.timestamp(preDate)) 315 | pretime = self.get_rls_pretime(pre) 316 | 317 | self.on_rss_entry( 318 | '{5}{6}[{0}] {10}{1}{2} {8}[{3}] {9}{4}{7}'.format( 319 | category, url, title, size, pretime, self.BOLD, 320 | self.RED, self.END, self.GREEN, self.YELLOW, 321 | self.BLACK)) 322 | FILE = open(self.announce_entries, "a") 323 | FILE.write("{}\n".format(id_announce)) 324 | FILE.close() 325 | 326 | threading.Timer( 327 | feeds['announce_delay'], self.announce_refresh).start() 328 | 329 | def request_refresh(self): 330 | FILE = open(self.request_entries, "r") 331 | filetext = FILE.read() 332 | FILE.close() 333 | 334 | for feed in feeds['request']: 335 | d = feedparser.parse(feed) 336 | for entry in d.entries: 337 | id_request = '{0}{1}'.format( 338 | smart_str(entry.link), 339 | smart_str(entry.title).split(' - ')[0].replace(' ', '.')) 340 | if id_request not in filetext and\ 341 | any([x not in id_request for x in blacklist['request']]): 342 | title = smart_str( 343 | entry.title).split(' - ', 1)[0].replace(' ', '.') 344 | url = smart_str(entry.link) 345 | self.on_rss_entry( 346 | '{2}{4}[REQUEST]{3} {0}: {6}{1}{5}'.format( 347 | title, url, self.BOLD, self.BLACK, 348 | self.PURPLE, self.END, self.BLUE)) 349 | FILE = open(self.request_entries, "a") 350 | FILE.write('{}\n'.format(id_request)) 351 | FILE.close() 352 | 353 | threading.Timer( 354 | feeds['request_delay'], self.request_refresh).start() 355 | 356 | 357 | def main(): 358 | 359 | usage = './wookie.py or \n\n'\ 360 | ' to run wookie in standard mode\n'\ 361 | ' to run wookie in detached screen' 362 | parser = optparse.OptionParser(usage=usage) 363 | (options, args) = parser.parse_args() 364 | if len(args) == 1 and ( 365 | args[0] == 'start' or 366 | args[0] == 'screen' or 367 | args[0] == 'run'): 368 | bot = _wookie() 369 | else: 370 | parser.print_help() 371 | parser.exit(1) 372 | 373 | try: 374 | if args[0] == 'screen': 375 | wookie['mode'] = 'screen' 376 | os.system('{0} {1}/./wookie.py run'.format( 377 | wookie['start_bot'], os.path.dirname( 378 | os.path.realpath(__file__)))) 379 | sys.exit(1) 380 | 381 | bot.connect( 382 | network['server'], network['port'], 383 | network['bot_nick'], network['bot_name'], 384 | ssl=network['SSL'], ipv6=network['ipv6']) 385 | bot.start() 386 | 387 | except OSError as error: 388 | print(error) 389 | sys.exit(1) 390 | except irclib.ServerConnectionError as error: 391 | print (error) 392 | sys.exit(1) 393 | 394 | if __name__ == "__main__": 395 | main() 396 | --------------------------------------------------------------------------------