├── .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 |
wookie
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 |
--------------------------------------------------------------------------------