Compare commits

..

680 commits

Author SHA1 Message Date
27bd0c50c1 server: Fix on_disconnect AttributeError when irc library lacks the method
All checks were successful
continuous-integration/drone/push Build is passing
Replace super().on_disconnect() call (absent in some irc library versions)
with self.jump_server() which is the actual reconnect method provided by
SingleServerIRCBot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 15:55:24 +07:00
9d7c278d1a bot: Fix sequential message processing with proper consumer pool
All checks were successful
continuous-integration/drone/push Build is passing
Replace the flawed cnsr_thrd_size threshold with cnsr_active, which
tracks the number of consumers currently executing a task. A new
consumer thread is now spawned the moment the queue is non-empty and
all existing consumers are busy, enabling true parallel execution of
slow and fast commands. The pool is capped at os.cpu_count() threads.

- bot.py: replace cnsr_thrd_size with cnsr_active + cnsr_lock + cnsr_max
- consumer.py: increment/decrement cnsr_active around stm.run(), remove
  itself from cnsr_thrd under the lock, mark thread as daemon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:08:27 +07:00
310f933091 bot: fix duplicate unregister KeyError and improve connection error logging
All checks were successful
continuous-integration/drone/push Build is passing
Silently ignore KeyError when unregistering an already-removed FD from
the poll loop (servers can queue multiple close events). Also include
the exception message when a server connection fails at startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:34:14 +07:00
26282cb81d server: Replace hand-rolled IRC with irc (jaraco) library
Switch the IRC server implementation from the custom socket-based parser
to the irc Python library (SingleServerIRCBot), gaining automatic
exponential-backoff reconnection, built-in PING/PONG handling, and
nick-collision recovery for free.

- Add IRCLib server (server/IRCLib.py) extending ThreadedServer:
  _IRCBotAdapter wraps SingleServerIRCBot with a threading.Event stop
  flag so shutdown is clean and on_disconnect skips reconnect when
  stopping. subparse() is implemented directly for alias/grep/rnd/cat.
- Add IRCLib printer (message/printer/IRCLib.py) calling
  connection.privmsg() directly instead of building raw PRIVMSG strings.
- Update factory to use IRCLib for irc:// and ircs://; SSL is now
  passed as a connect_factory kwarg rather than post-hoc socket wrapping.
- Add irc to requirements.txt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:34:14 +07:00
de2c37a54a matrix: Add Matrix server support via matrix-nio
Implement Matrix protocol support with MatrixServer (ThreadedServer subclass),
a Matrix message printer, factory URI parsing for matrix:// schemes, and
matrix-nio[e2e] dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:53:15 +07:00
622159f6b5 server: Add threaded server implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:47:41 +07:00
32ebe42f41 Don't compile for arm (requires rust ?????)
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-07 23:38:49 +01:00
ea0ec42a4b openai: Add commands list_models and set_model 2025-02-07 23:38:23 +01:00
23f043673f Add openai module 2025-02-07 21:41:02 +01:00
ac432fabcc Keep in 3.11
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-10-15 01:19:30 +02:00
38b5b1eabd Also build for arm64
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-10-14 23:01:11 +00:00
84fef789b5 Ignore decoding error when charset is erroneous
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-23 09:20:21 +02:00
45a27b477d syno: Fix new service URL
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-08 19:07:12 +02:00
a8472ecc29 CI: Fix build on arm
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-18 15:27:38 +01:00
861ca0afdd Try to connect multiple times (with different servers if any)
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-01-17 21:55:25 +01:00
9f83e5b178 Dockerfile: Point home into volume
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-06 14:10:30 +01:00
68c61f40d3 Rework Dockerfile and run as user
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-06 12:52:09 +01:00
60a9ec92b7 Rework Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-11 10:46:53 +02:00
8dd6b9d471 Fix a strange problem with saved PID file between runs
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-01 00:47:14 +01:00
13c643fc19 Add missing deps in container
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-08 14:04:42 +01:00
5ec2f2997b Add drone CI
Some checks reported errors
continuous-integration/drone/push Build was killed
2020-10-18 18:06:16 +02:00
faa5759645 suivi: add UPS tracking infos 2019-12-03 15:54:50 +01:00
37a230e70e suivi: kuse new laposte API to get infos 2019-12-03 15:54:50 +01:00
904ad72316 suivi: fix usps 2019-12-03 15:54:49 +01:00
f17f8b9dfa nntp: keep in memory latests news seen to avoid loop 2019-12-03 15:54:49 +01:00
d56c2396c0 repology: new module 2019-12-02 17:49:08 +01:00
b369683914 nntp: use timestamp from servers to handle desynchronized clocks 2019-11-09 14:46:32 +01:00
aee2da4122 Don't silent Exception in line_treat. Skip the treatment, but log. 2019-09-21 02:01:29 +02:00
1f2d297ea1 Update travis python versions 2019-09-21 01:16:52 +02:00
Maxence
b72871a8c2 Remove rms.py module
It should be replaced by a more generic:
!news https://www.fsf.org/static/fsforg/rss/events.xml which can be
aliased.
2019-09-21 01:15:13 +02:00
644a641b13 nntp: fix bad behaviour with UTF-8 encoded headers
Read-Also: https://tools.ietf.org/html/rfc3977
2019-09-10 15:50:17 +02:00
4499677d55 whois: don't use custom picture anymore 2019-08-22 15:51:36 +02:00
87b5ce842d cve: fix module with new cve website 2019-08-22 15:51:10 +02:00
144551a232 imdb: fix unrated content 2019-06-14 19:33:51 +02:00
7854e8628f news: reduce link URL by default 2019-02-03 12:59:17 +01:00
85c418bd06 feed: fix RSS link handling 2019-02-03 12:59:17 +01:00
20c19a72bc urlreducer: new function to be used in responses' treat_line 2019-02-03 12:59:17 +01:00
9417e2ba93 urlreducer: define DEFAULT_PROVIDER later 2019-02-03 12:59:17 +01:00
517bf21d25 imdb: fix series changed attributes 2019-02-03 12:59:17 +01:00
fa0f2e93ef whois: update module 2018-12-30 10:59:10 +01:00
b349d22370 events: ModuleEvent don't store function argument anymore 2018-12-30 00:42:21 +01:00
8605932702 imdb: follow imdb.com evolutions 2018-12-03 23:55:25 +01:00
10b8ce8940 smmry: add keywords options 2018-09-25 21:25:57 +02:00
445a66ea90 hooks: keywords can have optional values: place a question mark before =
{
	    "keyword?=X": "help about keyword (precise the default value if needed)"
	}
2018-09-25 21:25:02 +02:00
46541cb35e smmry: handle some more options 2018-09-25 21:23:46 +02:00
8224d11207 smmry: use URLStack from urlreducer module 2018-09-25 20:50:53 +02:00
15064204d8 smmry: don't ask to URLencode returned texts 2018-09-25 20:46:43 +02:00
67dee382a6 New module smmry, using https://smmry.com/ API 2018-09-25 20:43:21 +02:00
f1da640a5b tools/web: fix isURL function 2018-09-25 20:42:51 +02:00
2fd20d9002 nntp: Here it is! 2018-09-09 19:33:42 +02:00
31abcc97cf event: extract dict before call without init data 2018-09-09 19:17:56 +02:00
53fe00ed58 tools/web: new function to retrieve only headers 2018-08-30 12:59:55 +02:00
4a636b2b11 tools/web: follow redirection in URLConn 2018-08-30 12:59:28 +02:00
72bc8d3839 feed: accept RSS that begins with <rss> tag 2018-08-30 12:54:06 +02:00
5578e8b86e tools/web: split getURLContent function 2018-08-30 11:32:59 +02:00
3b99099b52 imdb: fix compatibility with new IMDB version 2018-08-30 11:20:36 +02:00
cd6750154c worldcup: update module to 2018 worldcup 2018-06-21 17:28:28 +02:00
b8741bb1f7 imdb: fix exception when no movie found 2018-06-20 17:46:52 +02:00
125ae6ad0b feed: fix RSS parsing 2018-06-06 14:50:45 +02:00
015fb47d90 events: alert on malformed start command 2018-06-06 14:50:45 +02:00
8a25ebb45b xmlparser: fix parsing of subchild 2018-06-06 14:50:45 +02:00
1887e481d2 sms: send result of command by SMS 2018-06-06 14:50:45 +02:00
342bb9acdc Refactor in treatment analysis 2018-06-06 14:50:44 +02:00
2af56e606a events: Use the new data parser, knodes based 2018-01-14 19:19:02 +01:00
4275009dea events: now support timedelta instead of int/float 2018-01-14 19:19:02 +01:00
f520c67c89 context: new function to define default data, instead of None 2018-01-14 19:19:02 +01:00
c3c7484792 In debug mode, display the last stack element to be able to trace 2018-01-14 19:19:02 +01:00
4cd099e087 xmlparser: make DictNode more usable 2018-01-14 19:19:02 +01:00
d528746cb5 datastore: support custom knodes instead of nemubotstate 2018-01-14 19:19:01 +01:00
Max
5cbad96492 [module] RMS upcoming locations 2018-01-14 19:19:01 +01:00
226ee4e34e ctfs: update module reflecting site changes 2018-01-14 19:19:01 +01:00
Max
e0d7ef1314 Fix https links when available, everywhere 2018-01-14 19:19:01 +01:00
Max
99384ad6f7 [suivi] Fix awkward USPS message 2018-01-14 19:19:01 +01:00
Max
ef4c119f1f [suivi] Add https when supported by service 2018-01-14 19:19:01 +01:00
Max
a7f4ccc959 [suivi] Fix Fedex tracking 2018-01-14 19:19:00 +01:00
Max
c23dc22ce2 [suivi] Fix colissimo tracking 2018-01-14 19:19:00 +01:00
bb0e958118 grep: allow the pattern to be empty 2018-01-14 19:19:00 +01:00
b15d18b3a5 events: fix event removal 2018-01-14 19:19:00 +01:00
5646850df1 Don't launch timer thread before bot launch 2018-01-14 19:19:00 +01:00
30ec912162 daemonize: fork client before loading context 2018-01-14 19:19:00 +01:00
28d4e507eb servers: call recv late 2018-01-14 19:18:59 +01:00
62cd92e1cb server: Rework factory tests 2018-01-14 19:18:59 +01:00
12ddf40ef4 servers: use proxy design pattern instead of inheritance, because Python ssl patch has benn refused 2018-01-14 19:18:59 +01:00
7a4b27510c Replace logger by _logger in servers 2018-01-14 19:18:59 +01:00
05d20ed6ee weather: handle units 2018-01-14 19:18:59 +01:00
f60de818f2 Virtualy move all nemubot modules into nemubot.module.* hierarchy, to avoid conflict with system/vendor modules 2018-01-14 19:18:58 +01:00
45fe5b2156 Refactor configuration loading 2017-09-19 07:25:34 +02:00
e49312e63e Remove legacy msg.text 2017-09-19 07:25:34 +02:00
a11ccb2e39 Remove legacy msg.cmds 2017-09-19 07:25:33 +02:00
fde459c3ff Remove legacy msg.nick 2017-09-19 07:25:33 +02:00
694c54a6bc imdb: switch to ugly IMDB HTML parsing 2017-09-19 07:25:33 +02:00
1dae3c713a tools/web: new option to remove callback from JSON files 2017-09-19 07:25:33 +02:00
89772ebce0 whois: now able to use a CRI API dump 2017-09-19 07:25:33 +02:00
aa81aa4e96 dig: better parse dig syntax @ and some + 2017-09-19 07:25:33 +02:00
3c7ed176c0 dig: new module 2017-09-19 07:25:33 +02:00
6dda142188 shodan: introducing new module to search on shodan 2017-09-19 07:25:33 +02:00
dcb44ca3f2 tools/web: new parameter to choose max content size to retrieve 2017-09-19 07:25:33 +02:00
0a576410c7 cve: improve read of partial and inexistant CVE 2017-09-19 07:25:32 +02:00
128afb5914 disas: new module, aim to disassemble binary code. Closing #67 2017-09-19 07:25:32 +02:00
39b7b1ae2f freetarifs: new module 2017-09-19 07:25:32 +02:00
09462d0d90 suivi: support USPS 2017-09-19 07:25:32 +02:00
b8f4560780 suivi: support DHL 2017-09-19 07:25:32 +02:00
281d81acc4 suivi: fix error handling of fedex parcel 2017-09-19 07:25:32 +02:00
29817ba1c1 pkgs: new module to display quick information about common softwares 2017-09-19 07:25:32 +02:00
b517cac4cf Fix module unloading 2017-09-19 07:25:32 +02:00
f81349bbfd Store module into weakref 2017-09-19 07:25:32 +02:00
ce012b7017 datastore/xml: handle entire file save and be closer with new nemubot XML API 2017-09-19 07:25:32 +02:00
Max
e3b6c3b85e Set urlreducer to use https 2017-09-19 07:25:31 +02:00
39056cf358 tools/xmlparser: implement writer 2017-08-23 21:13:39 +02:00
f16dedb320 openroute: new module providing geocode and direction instructions
Closing issue #46
2017-08-23 21:13:39 +02:00
171297b581 tools/web: new option decode_error to decode non-200 page content (useful on REST API) 2017-08-23 21:13:39 +02:00
3267c3e2e1 tools/web: display socket timeout 2017-08-23 21:13:39 +02:00
aad777058e cve: update and clean module, following NIST website changes 2017-08-23 21:13:38 +02:00
f633a3effe socket: limit getaddrinfo to TCP connections 2017-07-19 10:36:28 +02:00
bbfecdfced events: fix help when no event is defined 2017-07-19 10:36:28 +02:00
94ff951b2e run: recreate the sync_queue on run, it seems to have strange behaviour when created before the fork 2017-07-19 10:36:28 +02:00
a5479d7b0d event: ensure that enough consumers are launched at the end of an event 2017-07-19 10:36:28 +02:00
0a3744577d rename module nextstop: ratp to avoid import loop with the inderlying Python module 2017-07-19 10:36:27 +02:00
67cb3caa95 main: new option -A to run as daemon 2017-07-19 10:36:27 +02:00
2265e1a096 Use getaddrinfo to create the right socket 2017-07-19 10:36:27 +02:00
b6945cf81c Try to restaure frm_owner flag 2017-07-19 10:36:27 +02:00
e8809b77d2 When launched in daemon mode, attach to the socket 2017-07-19 10:36:27 +02:00
9d446cbd14 Deamonize later 2017-07-19 10:36:27 +02:00
cde4ee05f7 Local client now detects when server close the connection 2017-07-19 10:36:27 +02:00
ac0cf729f1 Fix communication over unix socket 2017-07-19 10:36:27 +02:00
35e0890563 Handle multiple SIGTERM 2017-07-19 10:36:27 +02:00
58c349eb2c suivi: add fedex 2017-07-19 10:36:26 +02:00
bcd57e61ea suivi: use getURLContent instead of call to urllib 2017-07-19 10:36:26 +02:00
0be6ebcd4b tools/web: fill a default Content-Type in case of POST 2017-07-19 10:36:26 +02:00
b4218478bd tools/web: improve redirection reliability 2017-07-19 10:36:26 +02:00
6ac9fc4857 tools/web: forward all arguments passed to getJSON and getXML to getURLContent 2017-07-19 10:36:26 +02:00
8a96f7bee9 Update weather module: refleting forcastAPI changes 2017-07-19 10:36:26 +02:00
7791f24423 modulecontext: use inheritance instead of conditional init 2017-07-16 21:17:48 +02:00
b809451be2 Avoid stack-trace and DOS if event is not well formed 2017-07-16 21:17:48 +02:00
dbcc7c664f [nextstop] Use as system wide module 2017-07-16 21:17:48 +02:00
8de31d784b Allow module function to be generators 2017-07-16 21:17:48 +02:00
f4216af7c7 Parse server urls using parse_qs 2017-07-16 21:17:48 +02:00
cf8e1cffc5 Format and typo 2017-07-16 21:17:48 +02:00
97a1385903 Implement socket server subparse 2017-07-16 21:17:48 +02:00
764e6f070b Refactor file/socket management (use poll instead of select) 2017-07-16 21:17:48 +02:00
6d8dca211d Use fileno instead of name to index existing servers 2017-07-16 21:17:48 +02:00
1c21231f31 Use super() instead of parent class name 2017-07-16 21:17:48 +02:00
2a3cd07c63 Documentation 2017-07-16 21:17:48 +02:00
b5d5a67b2d In debug mode, display running thread at exit 2017-07-16 21:17:48 +02:00
4c11c5e215 Handle case where frm and to have not been filled 2017-07-16 21:17:48 +02:00
a0c3f6d2b3 Review consumer errors 2017-07-16 21:17:48 +02:00
7cf73fb84a Remove reload feature
As reload shoudl be done in a particular order, to keep valid types, and because maintaining such system is too complex (currently, it doesn't work for a while), now, a reload is just reload configuration file (and possibly modules)
2017-07-16 21:17:48 +02:00
6cd299ab60 New keywords class that accepts any keywords 2017-07-16 21:17:48 +02:00
fc14c76b6d [rnd] Add new function choiceres which pick a random response returned by a given subcommand 2017-07-16 21:17:48 +02:00
24eb9a6911 Can attach to the main process 2017-07-16 21:17:48 +02:00
38fd9e5091 Remove legacy prompt 2017-07-14 12:35:55 +02:00
a7d7013639 Fix and improve reload process 2017-07-14 12:35:55 +02:00
9dc385a32a New argument: --socketfile that create a socket for internal communication 2017-07-14 12:35:55 +02:00
150d069dfb New CLI argument: --pidfile, path to store the daemon PID 2017-07-14 12:35:55 +02:00
f160411f71 Catch SIGUSR1: log threads stack traces 2017-07-14 12:35:55 +02:00
b0678ceb84 Extract deamonize to a dedicated function that can be called from anywhere 2017-07-14 12:35:55 +02:00
57275f5735 Catch SIGHUP: deep reload 2017-07-14 12:35:55 +02:00
ec512fc540 Do a proper close on SIGINT and SIGTERM 2017-07-14 12:35:55 +02:00
7bc37617b0 Remove prompt at launch 2017-07-14 12:35:55 +02:00
1858a045cc Introducing daemon mode 2017-07-14 12:35:55 +02:00
1b108428c2 Fix issue with some non-text messages 2017-07-14 12:35:55 +02:00
08a7701238 whois: add @lookup keyword to perform research in the list 2017-07-14 12:35:55 +02:00
be9492c151 weather: don't show expire date if not provided 2017-07-14 12:35:30 +02:00
679f50b730 alias: fix lookup replacement when empty list 2017-07-14 12:30:21 +02:00
2334bc502a alias: add syntax to handle default variable replacement 2017-07-14 12:30:21 +02:00
c3f2c89c7c alias: only perform alias expansion on Command 2017-07-14 12:30:15 +02:00
920506c702 wolframalpha: avoid content that is not plaintext 2017-07-14 09:33:34 +02:00
aefc0bb534 event: don't forward d_init if None 2017-07-14 09:32:41 +02:00
52b3bfa945 suivi: add postnl tracking 2017-07-14 09:23:57 +02:00
c8c9112b0f syno: CRISCO now speaks utf-8 2017-07-14 08:56:37 +02:00
7a48dc2cef whois: new URL to pick picts 2016-09-19 01:01:44 +02:00
0efee0cb83 [mediawiki] parse Infobox 2016-07-30 21:11:38 +02:00
3301fb87c2 [more] line_treat over an array 2016-07-30 21:11:25 +02:00
3f2b18cae8 [mediawiki] Permit control of ssl and absolute path through keywords 2016-07-30 21:09:18 +02:00
e9cea5d010 Fix events expiration 2016-07-29 03:13:37 +02:00
Max
f2c44a1108 Add virtual radar flight tracking module 2016-07-23 00:17:23 +02:00
Max
f15ebd7c02 [suivi] Fix TNT tracking 2016-07-23 00:17:23 +02:00
91b550754f [cve] Reflects site changes 2016-07-08 22:27:45 +02:00
abf810209e [alias] Fix empty error message 2016-04-18 17:21:51 +02:00
358499e6d5 Expect IM keyword argument in command to be at the begining of the args list 2016-04-18 17:21:51 +02:00
5fae67255b Log Python version 2016-04-08 12:55:24 +02:00
2c3d61495f Welcome in 2016... Happy new year! 2016-04-08 12:55:24 +02:00
a05821620d [alias] Allow arguments only on Command 2016-04-08 12:55:23 +02:00
26668c81b1 Update README 2016-03-01 16:56:19 +01:00
663e5e7207 Don't force python3.3 2016-03-01 16:56:18 +01:00
6ad979a5eb Fix event/timer issue if very close to 0 2016-01-29 16:52:52 +01:00
09e3b082c1 [alias] Give near alias in case of error 2016-01-24 18:44:28 +01:00
ff6460b92e Fix IRC message parameter escape 2016-01-19 18:45:14 +01:00
d028afd09e [alias] use subparse method 2016-01-19 14:50:47 +01:00
bd2eff83b7 [alias] Use alias command to display and define new aliases 2016-01-15 18:56:56 +01:00
645c18c981 [grep] use subparse feature 2016-01-12 23:54:26 +01:00
1d13d56dce [cat] New module performing cat like action 2016-01-12 23:53:43 +01:00
277d55d521 Add subparse method in context, that use server parser 2016-01-12 18:09:22 +01:00
d705d351c0 [grep] Add @nocase option, --ignore-case like 2016-01-12 16:50:49 +01:00
9ff8a3a02b [grep] raise an IMException if pattern not found 2016-01-12 16:49:51 +01:00
1d18305870 [grep] Add -o option 2016-01-12 16:39:01 +01:00
009ab08821 [man] Dusting + fill help 2015-12-28 19:20:31 +01:00
313c693d48 [imdb] Dusting + fill help 2015-12-28 19:15:32 +01:00
c9801ee2f7 [mapquest] Dusting + fill help 2015-12-28 19:10:00 +01:00
a089efff1a [more] Don't append space after a cut not ended by space 2015-12-28 19:09:41 +01:00
d4b6283e23 [github] new command to retrieve SSH keys 2015-12-28 19:09:20 +01:00
a3236cd67a [github] Dusting + fill help 2015-12-28 19:08:33 +01:00
274836e39a [github] Use default HTTP request timeout 2015-12-28 19:08:07 +01:00
707131023a [urlreducer] add some checks 2015-12-23 12:51:16 +01:00
57c460fc9c Simplify date extraction 2015-12-23 12:50:25 +01:00
cd0dbc4cc2 Xmlparser: parser for lists and dicts 2015-12-21 14:50:48 +01:00
d59f629dd9 Xmlparser: new class that just store one node, futher nodes will be parsed 2015-12-21 14:50:48 +01:00
1e29061bc9 [urlreducer] Framalink is in fact LSTU 2015-12-21 14:50:48 +01:00
e03d803ae0 [wolframalpha] Servers take a long times to respond theses days :( 2015-12-21 14:50:47 +01:00
f47aa8c478 Load module data on first access 2015-12-21 14:50:47 +01:00
6fc6561186 [alias] Fix parsing error when creating a (not allowed) spaced alias 2015-12-21 14:50:47 +01:00
ea8656ce0d Refactor command help: use hookmanager to get command help instead of custom search 2015-11-18 20:21:06 +01:00
0ba763f8b1 Display miss string only if no hook match on a full message treatment 2015-11-18 20:21:05 +01:00
43c42e1397 Rework hook managment and add some tests 2015-11-18 20:21:05 +01:00
926648517f Add config to package setup 2015-11-18 19:59:00 +01:00
31d93734a6 Fixed empty module configuration 2015-11-18 19:58:46 +01:00
e83c4091bf Avoid catchall DirectAsk 2015-11-18 19:58:26 +01:00
7ae7e381c3 [alias] Forward command keywords 2015-11-18 19:58:03 +01:00
f27347f028 [grep] Introducing new module that perform grep like action on subcommand 2015-11-13 17:32:58 +01:00
38412c1c16 Suggest command(s) on typo 2015-11-13 17:06:28 +01:00
2ebd86b80f [events] Avoid catchall hook 2015-11-13 17:06:10 +01:00
0f4a904a77 Log configuration loading 2015-11-13 17:05:52 +01:00
36cfdd8861 Added check and match module defined functions to hooks 2015-11-13 17:05:27 +01:00
11bdf8d0a1 [cve] Dusting module 2015-11-08 01:12:21 +01:00
00fa139e54 [syno] Dusting module 2015-11-08 01:12:21 +01:00
1ef54426bc [networking/whois] improve netwhois response by using normalized API fields 2015-11-08 01:12:20 +01:00
6aef54910e [networking/whois] New function to get domain availability status 2015-11-08 01:12:20 +01:00
a4e6e4ce84 [more] Fix append_content behaviour: initialize a list, don't convert string to char list 2015-11-08 01:12:20 +01:00
c4a7df7a6f [spell] Dusting module 2015-11-07 10:06:06 +01:00
3a1ce6c9e8 [ddg] Don't include empty definition in global results 2015-11-05 14:32:34 +01:00
c06fb69c8b Extract tools.config as config module 2015-11-03 16:53:49 +01:00
f39a0eac56 Refactors hooks registration 2015-11-03 16:53:49 +01:00
49d7e4ced6 Hooks: add global methods to restrict read/write on channels 2015-11-03 16:35:19 +01:00
de2e1d6216 Remove Message.receivers, long time deprecated 2015-11-03 07:26:01 +01:00
ea9829b341 Check command keywords using keyword help (passed in @hook) 2015-11-03 07:23:21 +01:00
70b52d5567 [translate] Refactor module, use keywords 2015-11-03 07:22:55 +01:00
8ff0b626a2 Update help of module using keywords 2015-11-03 07:22:26 +01:00
979f1d0c55 [more] Don't display the count string if the message is alone 2015-11-03 07:22:16 +01:00
9790954dfc Hooks can now contain help on optional keywords 2015-11-03 07:22:01 +01:00
c6aa38147b Include some forgotten module in reload process 2015-11-03 07:21:49 +01:00
8b4f08c5bd Replace IRCException by IMException, as nemubot is not only built for IRC 2015-11-03 07:21:06 +01:00
ac33ceb579 Remove dead or useless code 2015-11-03 07:20:52 +01:00
9935e038fc [man] num variable wasn't used here 2015-11-03 07:20:28 +01:00
f496c31d1c Help: don't append space character before ':' when the usage key is None 2015-10-31 17:45:50 +01:00
Max
c6e1e9acb2 [framalink] Update regex, clean up code 2015-10-29 16:34:58 +01:00
Max
1e36846265 [framalink] Fix ycc shortner 2015-10-29 16:34:58 +01:00
Max
04d5be04fa [suivi] Add TNT support 2015-10-29 16:34:58 +01:00
Max
3cb9a54cee [suivi] Code cleanup 2015-10-29 16:34:58 +01:00
Max
497263eaf7 [suivi] improve the suivi module
* Add multiple arguments/tracking numbers support
* Make it easier to add new tracking services
* Simplified to just one hook to which we can specify trackers using the
names variables

(correct typo in framalink comments)
2015-10-29 16:34:58 +01:00
e4d67ec345 Use Channel class when creating Server 2015-10-29 15:25:54 +01:00
2fdef0afe4 addChild should return a boolean 2015-10-28 10:55:03 +01:00
c560e13f24 Rework XML parser: part 1
This is the first step of the parser refactoring: here we change
	the configuration, next step will change data saving.
2015-10-28 10:55:02 +01:00
92530ef1b2 Server factory takes initializer dict 2015-10-28 10:55:02 +01:00
59ea2e971b Refactor modules that used nemubot XML parser due to previous commit 2015-10-28 10:55:02 +01:00
2b96c32063 [ddg] Split the module in two: ddg for search and urbandict for urbandictionnary 2015-10-27 23:05:49 +01:00
Max
aca073faff [framalink] Fix framalink quoting; add @provider
!framalink now allows the provider to be specified using the @provider
parameter.
2015-10-27 23:05:49 +01:00
Max
7ce9b2bb4c [framalink] Add error handling (invalid URLs) 2015-10-27 23:05:49 +01:00
5141a0dc17 tools/web: simplify regexp and typo 2015-10-27 23:05:38 +01:00
56c43179f3 tools/web: use core xml minidom instead of nemubot xml parser 2015-10-27 23:04:54 +01:00
089823d884 [rnd] New command to choice between cmd 2015-10-20 18:02:02 +02:00
7400957ac2 Bump version 4.0.dev3 2015-10-20 18:02:02 +02:00
60f7c6eea7 Place MessageTreater in context 2015-10-20 18:02:01 +02:00
4af108265b Split and rewrite message treatment from consumers 2015-10-20 18:02:01 +02:00
a4fd04c310 Remove print unhandled in daemon mode 2015-10-20 18:02:01 +02:00
f9f54989fe Fix logger level filtering 2015-10-20 18:02:01 +02:00
f9ee107403 SocketServer: able to connect to Unix socket 2015-10-20 18:02:01 +02:00
6c244cffa0 Server: add a socket listener, able to accept client on Unix or TCP socket 2015-10-20 18:02:00 +02:00
c94d9743dd Add --logfile option 2015-10-20 18:02:00 +02:00
5e95f50fb6 Expand argument paths 2015-10-20 18:02:00 +02:00
e925c47961 New function requires_version if module want to restrict to some version for compatibility 2015-10-20 18:00:31 +02:00
aee8545e65 Fix exception if no owner defined 2015-10-20 18:00:25 +02:00
39b7ecdaa4 Add keyworded arguments in command received 2015-10-20 16:27:00 +02:00
a1e7a7cff8 Add test for socket printer 2015-10-20 16:25:39 +02:00
981f6cc66c Run core tests in CI 2015-10-19 17:15:42 +02:00
dbca402fe7 [alias] Use title in response 2015-10-19 17:15:42 +02:00
3004c01db4 tools/web: set timeout to 7 secs 2015-10-19 17:15:42 +02:00
c0ce0ca263 Server Factory: Handle URL arguments without value 2015-10-19 17:15:42 +02:00
2f1f573af2 Fix abstract name of to_server_string function 2015-10-19 17:15:42 +02:00
ffc8fe40c3 Add a build system 2015-10-16 16:39:51 +02:00
fd99deed1d tools/web: colon char in URL precedes optional // 2015-10-15 19:26:09 +02:00
55e6550cb1 tools/web: factorize getNormalizedURL 2015-10-13 16:23:55 +02:00
76ec0d26b4 Modules: avoid unhandled exception in all_post 2015-10-13 16:23:51 +02:00
68e357d037 Initialize an empty module configuration if it has any (sentinel value) 2015-10-13 16:23:35 +02:00
aa4050f6cd [framalink] some refactor 2015-10-13 16:23:33 +02:00
2cd8f70cdc [networking] Dusting module 2015-10-12 13:37:53 +02:00
eb95480c8f [news] normalize URL before performing a join 2015-10-12 13:10:58 +02:00
20105e7d98 tools/web: add a URL normalizer function 2015-10-12 13:10:58 +02:00
7102e08000 tools/feed: hardened parser 2015-10-10 23:20:52 +02:00
04dcf07fb2 tools/web: use standard unescape instead of custom function when available 2015-10-09 17:56:34 +02:00
fd8567c60c Fix module unload and reload 2015-10-08 18:28:49 +02:00
db70974504 [alias] Fix old issues 2015-10-08 18:25:57 +02:00
Max
ece42ffe47 [urlshortner] Add framalink support 2015-10-09 01:47:42 +01:00
Max
c55e66dd70 [tools/web] Add header param to getContentUrl()
Add the possibility to specify headers when querying websites.
2015-10-08 02:47:42 +01:00
684806baaf Help command: skip discovery of command without name 2015-10-07 18:22:01 +02:00
ff2911dbd3 Add a subtreat method in modulecontext
This feature allows module to call the message treatment process on a crafted message
2015-10-07 18:22:01 +02:00
a6a10b78d1 Fill help in some modules 2015-10-07 18:22:01 +02:00
Max
e1310516fa [alias] Fix argument consumption, allow multiple usage of same var 2015-10-07 00:14:37 +01:00
Max
461c62f596 Fix alias ranges
Args are now consumed when in ranges, and ranges with 1 bound now work
2015-10-06 23:23:42 +01:00
57ba0d5db9 [networking] Fix traceurl: trace all URL even if an error occurs 2015-10-03 16:47:20 +02:00
c812fd8c16 As the char ':' is not valid in URL, don't expect it 2015-10-03 16:47:08 +02:00
7970fca93a Use with section for locking threadsafe region (instead of raw calls to acquire/release) 2015-09-28 17:21:03 +02:00
080ab9a626 Fix bad event behaviour: if an event ends in less than 6 seconds, it was executed in the event creator thread (blocking it until the event end) 2015-09-28 17:20:45 +02:00
ff605756ff [news] Add support for RSS feeds and catch ExpatError when trying to parse a bad URL 2015-09-28 12:59:00 +02:00
59aff52ce1 Change the behaviour of send_response in module 2015-09-28 12:45:11 +02:00
283b0d006e Add a new builtin: !echo 2015-09-28 12:45:10 +02:00
b66d7d30ed Accelerate shutdown 2015-09-28 12:27:23 +02:00
f66ed07496 Lock select lists to avoid invalid states (particularly on closing) 2015-09-28 11:59:38 +02:00
ae7526dd96 Fix double exception when invalid file descriptor found in select 2015-09-28 11:59:37 +02:00
dda78df9d2 Add new action queue, synchronized with main thread for prompt like actions (conf loading, exit, ...) 2015-09-28 11:59:37 +02:00
3cfbfd96b0 Let main thread manage consumer threads 2015-09-28 11:59:37 +02:00
ee1910806c [news] Introduce new module News: it fetchs atom feed from a website and display it 2015-09-28 11:53:55 +02:00
a4f4bb799c Extract atom from networking module to core 2015-09-28 11:53:29 +02:00
471feca8fb [networking.atom] use Datetime to store internal dates and can get an ordered list of elements 2015-09-28 11:53:11 +02:00
bbf5acafbb [mediawiki] fix OpenSearch: can have empty description 2015-09-25 15:48:09 +02:00
7a1ad6430c [whois] update module
* change intra-bocal URL to fix CertificateError
	* fix 23.tf trombi URL
	* add a catch when trying trombi URL
2015-09-24 11:29:51 +02:00
4cb8b0f1a6 Improve help
On hook declaration, we can now add a help and/or a help_usage argument
	to provide a simple way to the user to be informed.

	For example:

	```python
	@hook("cmd_hook", "news", help_usage={"URL": "Display the latests news from a given URL"})
	def cmd_news(msg):
	    [...]
	```

	will be displayed on !help !news as:

	> Usage for command !news from module news: !news URL: Display the latests news from a given URL

	Or for module commands help:

	```python
	@hook("cmd_hook", "news", help="display latests news")
	def cmd_news(msg):
	    [...]
	```

	will be displayed on !help mymodule (assuming this hook is in the
	module named mymodule) as:

	> Available commands for module news: news: display latests news

	Obviously, both `help` and `help_usage` can be present. If `help_usage`
	doesn't exist, help on usage will display the content of help.
2015-09-24 11:29:51 +02:00
4f7d89a3a1 [ycc] Dusting module, now named tinyurl 2015-09-24 11:29:50 +02:00
be776405e3 Dusting URL stacking modules: fixing error when using channel list as response 2015-09-24 11:29:50 +02:00
3c51b1f03b Add assertion on class initialization 2015-09-24 11:29:49 +02:00
ecd9457691 Help: display on the right place, not always to private conversation 2015-09-24 11:29:49 +02:00
35ba5c03c9 [more] Allow method chaining 2015-09-24 11:29:49 +02:00
Max
8018369800 Add global tracking hook 2015-09-24 11:29:48 +02:00
d74a9067c0 [bonneannee] Command for the nextyear doesn't require argument anymore 2015-09-24 11:29:48 +02:00
d5f07ec338 Display a basic error to IM user on uncatched exception 2015-09-24 11:29:47 +02:00
Max
e915c4930c Fix #2 ; Add chronopost and colis privé support 2015-09-24 11:29:47 +02:00
760da8ef61 tools.web: don't try to striphtml content that is not str or buffer 2015-09-24 11:29:47 +02:00
9b2bc27374 tools.web: restore Python3.3 behavior: don't check server certificate 2015-09-24 11:29:46 +02:00
8988dd0d41 striphtml: also convert ´ and collapse multiple space, as HTML display do 2015-09-24 11:29:46 +02:00
9fa8902f1a Invalid fd are < 0, not only -1 2015-09-24 11:29:46 +02:00
beeb5573e1 Define class variables in __init__ 2015-09-24 11:29:45 +02:00
ac3ed0d492 [alias] huge refactoring 2015-09-24 11:29:44 +02:00
9686f36522 Add a function to guess the closest word for a miss input 2015-09-24 11:29:44 +02:00
ec9481e65b [networking] Add !title command to display the title of a page 2015-09-24 11:29:43 +02:00
e837f9c8e5 Improve formating of size function and test it 2015-09-24 11:29:43 +02:00
969210a723 [networking] Avoid exception when port is not defined on socket error 2015-09-24 11:29:37 +02:00
2b0593a51e Add tool to calculate string distance 2015-09-23 18:06:21 +02:00
6147eef19b [mediawiki] Help user find the article he want to read if it doesn't exist 2015-09-23 10:57:25 +02:00
67cd66b922 [mediawiki] Handle # 2015-09-23 10:57:25 +02:00
Max
710896f711 [suivi] Now using nemubot.tools.web for queries 2015-08-26 12:18:24 +02:00
Max
5b039edb62 Updated tracking module to support colissimo 2015-08-26 12:18:24 +02:00
d1c28fc4a3 [networking] New function to get a list of watched URL 2015-08-26 12:18:24 +02:00
c27540eb87 web: reduce timeout from 15 to 7 seconds 2015-08-26 12:18:23 +02:00
88a8e0fe59 web: can make POST request 2015-08-26 12:18:23 +02:00
0208a5d552 Allow socket to print messages 2015-08-26 12:18:23 +02:00
a00c354287 Add a factory to help connecting to servers 2015-08-26 12:18:22 +02:00
d269468287 Let consumer parse the message instead of server 2015-08-26 12:18:22 +02:00
a1ac7d480d Split server message parsing from message retrieving 2015-08-26 12:18:22 +02:00
3c15d35fca [alias] Dusting module 2015-08-26 12:18:21 +02:00
4d51bc1fda Dusting modules 2015-08-26 12:18:20 +02:00
4bc8bc3c12 Dusting modules 2015-08-26 12:17:22 +02:00
Max
b5e4acdb70 [jsonbot] replacing spaces with %20 in queried url 2015-08-26 12:17:22 +02:00
Max
6e226f0fb3 [nextstop] Updated nextstop with destination option 2015-08-26 12:17:22 +02:00
b75c54419f [ddg] Dusting + !safeoff handling 2015-08-26 12:17:21 +02:00
787a5fd3da Web tool raise more IRCException 2015-08-26 12:17:21 +02:00
000c67e45e Can return Response in help_full function 2015-08-26 12:17:21 +02:00
ae4a303554 Fix #74 2015-08-26 12:17:21 +02:00
6415e4a697 [networking] Handle ConnectionError exceptions 2015-08-26 12:17:20 +02:00
3b8195b81b [alias] Variable replacement can operate on slice list.
In addition to ${1}, ${2}, ... you can now use slice: ${1:5} or ${1:}.
	The effect is to join arguments with a space character.
2015-08-26 12:17:20 +02:00
Max
dc681fdc35 [jsonbot] now taking all cmd args 2015-08-26 12:17:20 +02:00
Max
3cad591086 [imdb] Fixed grammar 2015-08-26 12:17:19 +02:00
Max
ea05b3014c [jsonbot] now supports json lists 2015-08-26 12:17:19 +02:00
0c960e984a [networking] fix watch pages that aren't text/html 2015-08-26 12:17:19 +02:00
487cb13e14 [weather] fix some hypothetical errors 2015-08-26 12:17:18 +02:00
d90c44de49 [nextstop] Update submodule 2015-08-26 12:17:18 +02:00
b44464d255 [networking] Log error when unable to restart a watch 2015-08-26 12:17:17 +02:00
26515677b8 Don't add new event after main thread stop 2015-08-26 12:17:12 +02:00
92895a7b1d XML parser: perform atomic save by moving a temporary file after the serialization 2015-06-24 20:06:01 +02:00
Max
0e76a6ed2a Added json parsing module 2015-06-24 20:06:00 +02:00
c7706bfc97 XML datastore: load will now automatically try to load backup 2015-06-24 20:06:00 +02:00
ab2eb405ca XML datastore: add file rotation for backup purpose 2015-06-24 20:05:59 +02:00
Ivan Delalande
8a7ca25d6f Change !yt to use youtube-dl
youtube-dl does a good job at extracting all the information from hundreds of
website, presenting it in a standardized way and staying up-to-date.

Signed-off-by: Ivan Delalande <colona@ycc.fr>
2015-06-24 20:05:59 +02:00
5f29fe8167 [alias] Allow anyone to remove an alias 2015-06-24 20:05:59 +02:00
889b129da3 [alias] Fix #86 2015-06-24 20:05:58 +02:00
ed6da06271 [alias] Fix #83 2015-06-24 20:05:58 +02:00
cacf216e36 [alias] Fix #85 2015-06-24 20:05:57 +02:00
Max
ef73516ceb Updated submodule 2015-06-24 20:05:57 +02:00
Max
73082e4109 Updated nextstop module 2015-06-24 20:05:57 +02:00
3ac151f888 [books] Fix API calls 2015-06-24 20:05:56 +02:00
9cf4b9becb Fix bot close
Tell consumer to stop their work
	Avoid error when select on closed fd
2015-06-24 20:05:56 +02:00
c0e6b26b0c [alias] Fix variable replacement in aliases 2015-06-24 20:05:55 +02:00
859b32abb7 [ctfs] Improve module 2015-06-24 20:05:55 +02:00
381cf13432 [networking] Allow anyone to remove a watch 2015-06-24 20:05:54 +02:00
Bob
f786dd1d43 ctfs plugins 2015-06-24 20:05:54 +02:00
f4a80e0fda New function in ModuleContext: call_hook 2015-06-24 20:05:54 +02:00
c86031ea32 Can use print with non string 2015-06-24 20:05:53 +02:00
d95de8c195 In some modules, raise ImportError to avoid module loading on errors 2015-06-24 20:05:53 +02:00
2e55ba5671 [ddg/wolframalpha] extract wolframalpha module and dusting 2015-06-03 19:39:16 +02:00
c1858fff3a Catch exception during module loading: just skip the module registration 2015-06-03 18:01:50 +02:00
91688875d4 [github] can use #ID as ID when looking for a particular issue 2015-06-03 17:20:32 +02:00
19ad2d7a32 [github] use msg.args instead of deprecated msg.cmds; fixes #80 2015-06-03 17:11:25 +02:00
04023e945e Fix module parseresponse when more.Response is used as response 2015-06-03 17:00:28 +02:00
8d91ad31fb [whois] Module try to find a recent photo 2015-06-03 15:30:28 +02:00
889e376254 [whois] Module try to find a recent photo 2015-06-03 15:30:27 +02:00
63a6654331 [yt] Improve module: track last video URL 2015-06-03 15:30:27 +02:00
Bob
b3274c0dc7 New module youtube-title: that retrieve title from a youtube link 2015-06-03 15:30:27 +02:00
fc500bc853 Tools.Web: fix charset detection on webpages 2015-06-03 15:30:26 +02:00
4be9f78104 [whois] New module from nbr23 bot 2015-06-03 15:30:17 +02:00
500e3a6e01 Compatibly with Python 3.4 2015-05-26 12:33:28 +02:00
fc1bc135df Importer: now compatible with Python 3.4 2015-05-26 12:33:16 +02:00
Maxence
481b1974c3 [laposte] handling no arguments 2015-05-25 16:44:32 +02:00
Maxence
9120cf56c2 Updated SAP transaction lookup module 2015-05-25 16:44:32 +02:00
Maxence
7d051f7b35 Added a La Poste tracking module 2015-05-25 16:44:31 +02:00
9c78e9df1d [cve] Merge multiple lines 2015-05-25 16:44:31 +02:00
65b5f6b056 [mediawiki] Improve parsing of recursive templates 2015-05-25 16:44:31 +02:00
40ff3d6eda Socket connection can now be made in IPv6 2015-05-25 16:44:30 +02:00
0fb58f0ff2 Use expat parser instead of SAX to parse XML files 2015-05-25 16:44:30 +02:00
0f2f14ddda XML datastore: new directory locking procedure
This new procedure use fcntl functions to lock the file during the
	life of the datastore instance.

	Now, locked directory error is not displayed if if nemubot is not
	correctly closed.
2015-05-25 16:44:30 +02:00
f19dd81a0d Update TODO item 2015-05-25 16:44:29 +02:00
2644d1bc02 [xmlparser] Fix date extraction when using old format 2015-05-25 16:44:29 +02:00
8bcceb641f Add a logger to module context on init 2015-05-25 16:44:29 +02:00
48ebc1b1f5 [speak] Avoid saying multiple identical message 2015-05-25 16:44:28 +02:00
002f2463a3 Extract hooks 2015-05-25 16:44:28 +02:00
c8d495d508 Split messages class into multiple files 2015-05-25 16:44:27 +02:00
57bbca4e7a Raise an exception when unable to open datastore, instead of returning False 2015-05-25 16:44:27 +02:00
1b9395ca37 Doc 2015-05-25 16:44:27 +02:00
418ff4f519 Datastore: add a method to create a new empty tree 2015-05-25 16:44:26 +02:00
4d7d1ccab2 Add unittest for IRCMessage 2015-05-25 16:44:26 +02:00
3d1a8ff2ba [mediawiki] improve output 2015-05-25 16:44:25 +02:00
c984493c79 Place events to a separate directory 2015-05-25 16:44:25 +02:00
06bc0a7693 IRC: allow empty host as ZNC seems to send empty one sometimes 2015-05-25 16:44:25 +02:00
806ff1d4a0 Move main code to __main__.py 2015-05-25 16:44:24 +02:00
e588c30044 Optimize imports 2015-05-25 16:44:16 +02:00
2e7a4ad132 Save timestamp in UTC format 2015-02-18 01:48:02 +01:00
dacb618069 Increasing version number due to significant changes 2015-02-13 12:58:45 +01:00
46268cb2cf [networking] Fix variable name conflict 2015-02-13 12:56:29 +01:00
28005e5654 Convert modules to new importer 2015-02-11 18:12:39 +01:00
1f5364c387 Reduce importance of importer; new tiny context for each module, instead of having entire bot context 2015-02-11 18:11:57 +01:00
bafc14bd79 add_server can now be used before context start 2015-02-10 00:42:38 +01:00
f66d724d40 Introducing data stores 2015-02-10 00:30:04 +01:00
e7fd7c5ec4 Arrange IRC server construction
reorder constructor argument to a more logical order
	on_connect can be a simple string or a callable
2015-02-09 23:07:30 +01:00
55b1eb5075 Allow data_path to be None (don't load or save data) 2015-02-09 17:31:32 +01:00
e7d37991b3 Allow print and print_debug with multiple arguments 2015-01-22 17:18:49 +01:00
e225f3b8d7 import ModuleState when needed 2015-01-05 10:18:40 +01:00
2b9c810e88 Fix quit 2015-01-05 03:07:41 +01:00
38aea5dd37 Lock the data directory to avoid concurent modification of XML files 2015-01-05 02:49:21 +01:00
06c85289e0 Don't import some nemubot module automatically 2015-01-05 02:49:06 +01:00
7c7b63634b [wip] in modules, changes import to reflect new directory structure 2015-01-05 02:49:05 +01:00
5a6230d844 [wip] changes import to reflect new directory structure 2015-01-05 02:48:49 +01:00
41f7dc2456 [wip] move files in order to have a clean directory structure 2015-01-04 15:14:35 +01:00
8aebeb6346 [cmd_server] rework due to previous prompt rework 2015-01-04 15:14:16 +01:00
fd6d9288f7 Rework prompt: add exception classes for errors and reload/quit 2015-01-02 16:17:44 +01:00
0d21b1fa2c Indicate full version in UserAgent HTTP header 2015-01-02 10:15:30 +01:00
1a04a107ac web tools: handle no route to host error 2015-01-02 08:01:18 +01:00
116c81f5b2 Use ipaddress module to store IP 2015-01-02 07:57:18 +01:00
0d4130b391 [events] ids don't have to be saved 2014-12-31 09:46:15 +01:00
17bbb000ad [networking] Oops, watchWebsite wasn't working; fixed 2014-12-29 07:50:27 +01:00
466ec31be7 [nextstop] Dusting 2014-12-29 07:50:18 +01:00
192a26b5ea Update README: add requirements part 2014-12-29 07:32:15 +01:00
7805f27458 Modify importer to work with Python 3.3 and above 2014-12-29 07:30:25 +01:00
463faed697 [web] new maximal downloaded size: 512k (old: 200k) 2014-12-20 10:27:13 +01:00
c691450111 [tpb] Give working magnet link 2014-12-20 07:28:12 +01:00
bf266dd21f [ycc] Dusting 2014-12-18 13:02:19 +01:00
d14fec4cec Modules: global dusting: call getJSON instead of making raw calls to urllib 2014-12-18 12:52:39 +01:00
66ec7cb7ca [tpb] More usefull information 2014-12-18 11:54:35 +01:00
6b6ff0cb56 New tool to convert some content to human readable strings 2014-12-18 11:52:43 +01:00
99106d26a9 Fix #70: new module tpb using an API with a TPB dump 2014-12-17 18:09:19 +01:00
a7b166498c xmlparser: don't manage errors at this level 2014-12-17 17:58:30 +01:00
0b06261d18 tools/web: allow empty Content-Type 2014-12-17 17:50:43 +01:00
dd285b67d1 fix netloc != hostname 2014-12-17 17:49:57 +01:00
a1c086a807 [networking] use getJSON 2014-12-17 16:00:06 +01:00
86fdaa4dd9 web: fix new usage of getURLContent in getJSON 2014-12-17 15:53:49 +01:00
02acad5968 argparse: -m to load modules 2014-12-17 13:07:45 +01:00
22a2ba76c4 argparse: add --no-connect option to disable autoconnect rules in configuration files 2014-12-17 12:53:47 +01:00
f575674d47 argparse: add version information option 2014-12-17 12:49:43 +01:00
3265006adb argparse: add verbosity level 2014-12-17 12:43:08 +01:00
65aa371fdc Use argparse to parse CLI argument 2014-12-17 12:37:14 +01:00
d6ea5736a5 Move xmlparser to tools 2014-12-17 12:06:28 +01:00
5dcf0d6961 [networking] integrate watchwebsite module to networking + doc and reworking 2014-12-17 12:06:11 +01:00
c75d85b88b [birthday] fix date saving for other people 2014-12-17 10:59:18 +01:00
c7baf6ecbe Date tool: can extract date with year and without hours 2014-12-17 10:59:18 +01:00
52cf7b5ad7 Date tool: fix forgotten import when extracting a date without hours 2014-12-17 10:59:17 +01:00
48149fadc1 [networking] update netwhois 2014-12-17 10:59:02 +01:00
f181d644b4 [networking] Refactor module 2014-12-17 10:54:57 +01:00
bbd928c6fa Prompt: documentation, factoring 2014-11-18 12:48:33 +01:00
a418ca860a Merge pull request #66 from Bobobol/v3.4
Add CVE module; fixes #60
2014-11-17 15:01:08 +01:00
Bob
0dd6036808 del xml 2014-11-17 14:56:08 +01:00
Bob
58d330c333 add cve module 2014-11-17 14:24:18 +01:00
23b60814b7 Remove dead code in importer 2014-11-14 15:46:48 +01:00
001ff35758 In servers list, display its state 2014-11-14 15:39:25 +01:00
fd5fbf6c6c Fix module load and reload 2014-11-14 15:38:48 +01:00
63cc770800 [cmd_server] launch and disconnect function doesn't exist anymore 2014-11-14 14:25:52 +01:00
093581f646 Fix missing import 2014-11-14 14:01:00 +01:00
2dfe1f0e9a PEP8 clean 2014-11-13 15:52:04 +01:00
e1aff6c4cf channel or nick required when creating a Response 2014-11-13 14:51:18 +01:00
4f27232fd4 [bonneannee] Unharcode channel to send message on 1 January 2014-11-10 17:05:18 +01:00
b6c5bf4f10 Move configuration file loading from prompt to tools 2014-11-10 17:02:23 +01:00
e17996d858 PEP8 clean 2014-11-10 13:02:13 +01:00
95deafe7af [speak] Fix error on non-TextMessage arrival 2014-11-10 13:01:03 +01:00
8dfd0f07cc IRC server: differentiate nick and username 2014-11-03 15:46:59 +01:00
745d2b0487 Events module: display an error if server doesn't exist when registering the event 2014-11-03 15:45:16 +01:00
77b897a1e3 Fix in modules: is_owner is now frm_owner 2014-11-03 15:43:39 +01:00
7c12f31d2c Log XML parsing errors 2014-11-03 10:14:53 +01:00
fafa261811 Update copyrights 2014-11-03 10:14:00 +01:00
0731803550 Don't call _open if it is not defined 2014-11-03 10:09:38 +01:00
5e097b5415 New attribute on Messages: frm_owner, indicating a message coming from the bot owner 2014-10-28 09:45:59 +01:00
f8884a53ec On exit, stop main loop 2014-10-28 09:45:59 +01:00
f927d5ab0a Convert nemuspeak as a module to nemubot 2014-10-27 18:40:04 +01:00
5e87843dda Mediawiki module: fetch namespaces list to hide categories 2014-10-22 07:38:53 +02:00
67f6d49fb8 Weather module: new nemubot version fix 2014-10-21 20:55:39 +02:00
fe709e630f SMS module: new nemubot version fix 2014-10-21 17:06:25 +02:00
bd92f64449 More module: ensure that never return None 2014-10-10 23:15:11 +02:00
6c89f80bcf Fix sample configuration 2014-10-10 23:14:32 +02:00
41c33354c3 Choose another nick on nick collision 2014-10-10 23:14:05 +02:00
4776fbe931 Modify context reload for better maintainability 2014-10-09 07:39:38 +02:00
4dd837cf4b Change add_server behaviour, fix IRC parameters parsing, can use with Python statement for managing server scope 2014-10-09 07:37:52 +02:00
f9ee1fe898 Can use countdown without timezone 2014-10-09 07:31:21 +02:00
dfde4c5f49 New message processing 2014-10-07 00:21:14 +02:00
981025610e Fix IRC message pretty-printer 2014-10-04 07:41:54 +02:00
020759fdab Decoding IRC message: use encoding from configuration file 2014-10-04 07:39:08 +02:00
49bfcdcae5 Bonneannee module: fix timezone 2014-10-04 07:34:30 +02:00
5d5030efe1 Syno module: add english language support; closing #64 2014-10-04 07:33:34 +02:00
ce9dec7ed4 Xmlparser module: getNodes is now a generator 2014-10-02 07:03:27 +02:00
ada19a221c In Response, nomore can now be a function 2014-10-02 06:59:54 +02:00
302add96ca Add a connected state for socket 2014-10-01 00:33:52 +02:00
1c1139df9f Autojoin on invitations 2014-10-01 00:17:36 +02:00
23c660ab57 Mediawiki module: can search through opensearch or classic search 2014-10-01 00:16:19 +02:00
c5a69f1bd0 Internal timezone is now UTC 2014-10-01 00:14:27 +02:00
32cb79344b Mediawiki module: fix links 2014-09-29 22:45:44 +02:00
74bb0caa1b Fix prompt commands: join, leave and part 2014-09-28 20:34:49 +02:00
99a5e8e5ad Can send command on connection, defined in configuration file 2014-09-27 23:57:19 +02:00
41da1c0780 Response class is now part of 'more' module.
This commit prepare the new message flow based on protocol independent messages.

	This commit changes the module API: you need to import the Response class manually at the begining of our module.
2014-09-27 12:39:44 +02:00
8f620b9756 XMLparser: precise unhandled type 2014-09-24 15:57:21 +02:00
5be1e97411 Calculate message size based on raw size, not on UTF-8 characters number 2014-09-24 15:56:46 +02:00
cdaad47b13 Ensure multiline message after line_treat are collapsed to one line 2014-09-24 10:53:16 +02:00
acded35e1a Pick events class from v4 2014-09-23 22:47:50 +02:00
04eccbe250 Remove some legacy stuff 2014-09-22 17:48:59 +02:00
0ab51d79ae Pong only on "ask" 2014-09-22 17:48:41 +02:00
314d410789 Logger identifier for server now depends on server identifier and is not global 2014-09-21 19:46:18 +02:00
a7830f709d WatchWebsite module: use w3m function from networking module 2014-09-20 00:15:06 +02:00
7a5c2d9786 Separate curl and w3m functions to use it from others modules 2014-09-20 00:14:19 +02:00
b184b27d4f Randomize the first fetch of watched pages; closing #33 2014-09-19 19:28:48 +02:00
8b819f097d Mediawiki: display an error when the article doesn't exist 2014-09-19 08:02:31 +02:00
880b2950d3 RATP: clean and update module 2014-09-19 01:42:37 +02:00
ae3c46e693 Xmlparser: check the attribute type is storable 2014-09-19 01:38:53 +02:00
dee6ec28fe Can deferred a treatment on each line responded (to save time at first fetch) 2014-09-18 08:22:59 +02:00
4d187f61e3 Fix exception during deletion of the first event in rare case 2014-09-18 08:08:46 +02:00
772d68a34d Response sender is not needed anymore, private channels are now better handled 2014-09-18 07:57:06 +02:00
5e202063d4 Improve stability: catch all kind of exception in main bot loop 2014-09-18 06:26:50 +02:00
a0a1ef8989 Always parse the same number of arguments; empty string != None 2014-09-17 06:59:40 +02:00
1beed6751b Books module: can search books writen by someone and read description of a given book; closing #65 2014-09-16 20:20:37 +02:00
edbfac2943 Alias module: avoid infinite recursion if an alias substitute by the same command 2014-09-15 07:55:35 +02:00
73acc00762 Handle IRC PART command 2014-09-15 01:07:43 +02:00
fa81fa5814 Handle channel creation in IRC server 2014-09-15 00:52:19 +02:00
7dc3b55c34 Parse most of IRC messages in server part instead of core 2014-09-13 23:50:32 +02:00
db22436e5d Handle server related or specific stuff out of the pure core, in the server part (PING, CAP, CTCP requests, ...) 2014-09-12 08:12:55 +02:00
8c52f75b6a Prepare hooks to be used for other things than Message 2014-09-11 21:20:56 +02:00
877041bb12 Message parsing is now a server part 2014-09-11 19:29:57 +02:00
d83b0d1b81 Report HTTP error to users 2014-09-10 21:33:28 +02:00
4cbf73c45a Rework on networking module 2014-09-10 21:28:47 +02:00
4f19f08c9f WatchWebsite module: raw content can be display in response 2014-09-10 12:19:25 +02:00
88219a773a Prompt now uses Python readline feature without need of rlwrap 2014-09-09 07:09:29 +02:00
5f86b35cf0 Fix context reload feature 2014-09-09 07:02:41 +02:00
1c847d11d6 allow_all doesn't exist anymore 2014-09-09 07:00:17 +02:00
28c86d461a Restore !help feature as standard command hook 2014-09-09 06:59:57 +02:00
d32a0cdc15 Update configuration sample with capabilities 2014-09-08 02:42:37 +02:00
f51ad21e91 Don't send CAP REQ if there is no compatible capabilities 2014-09-08 02:41:50 +02:00
95db63bf47 Capabilities negociation fully implemented 2014-09-08 02:36:19 +02:00
9b9c02fe29 Handle connection errors (like timeout) 2014-09-08 02:31:02 +02:00
eba4a07ed1 Event module: use time tag instead of now() as event start time 2014-09-08 02:28:34 +02:00
3ac40ac703 Handle fd/socket exception in select 2014-09-08 02:26:50 +02:00
cccee20cdf Pick from v4 message tag parser 2014-09-08 02:26:18 +02:00
ee14682c4f extractDate function is now in a separate Python module 2014-09-08 02:25:26 +02:00
04bde60482 Fix message decoding: not all parameters was decoded 2014-09-08 01:55:36 +02:00
c13173e62c Handle :***!znc@znc.in 2014-09-08 00:21:10 +02:00
76399a110f New module books: related to #65 2014-09-07 23:55:40 +02:00
8f17c0a977 Velib module: fix use without xml configuration file 2014-09-07 23:52:54 +02:00
8efc92edde Fix usage of parse_string 2014-09-07 23:51:20 +02:00
b63170244a IRC: capabilities negociation 2014-09-07 23:43:22 +02:00
028b7fd88d Fix PONG response: Messages params are not decoded 2014-09-05 01:48:01 +02:00
85ec2dcd01 New callback _on_connect, called after 001 numeric reply reception: currently it joins channels 2014-09-04 10:43:50 +02:00
c32f1579ee Fix PONG response when no registered input treatment 2014-09-04 09:56:53 +02:00
b0e457ffc9 Fix return of parselisten functions 2014-09-03 19:06:26 +02:00
7387fabee1 New module mediawiki 2014-09-02 21:21:06 +02:00
3bc53bb4ef Introducing new hooks manager
Currently, the manager use a naive implementation, this is
	mainly for architectural testing purpose.
2014-09-01 19:21:54 +02:00
29819c7874 Fix private and CTCP responses 2014-08-31 11:08:34 +02:00
038590c659 Server disconnection works properly 2014-08-31 11:07:23 +02:00
dcce36eb7c Implement more and next features as module instead of part of core 2014-08-31 01:55:28 +02:00
81593a493b (wip) use select instead of a thread by server. Currently read and write seems to work properly, we lost some code (like more, next, ...) that never should be in server part 2014-08-30 20:20:47 +02:00
28c1ad088b Move countdown to a separate module in tools 2014-08-29 17:13:28 +02:00
0a16321259 (wip) reworking of the Message class; backported from v4 2014-08-29 17:12:12 +02:00
0e26450d8f Rework CTCP responses and implement FINGER, PING and SOURCE 2014-08-29 12:25:25 +02:00
b4800643e1 Fix list function in prompt 2014-08-29 12:03:49 +02:00
8d1919a36b Backport part of v4 Bot class 2014-08-29 12:03:41 +02:00
a8fe4c5159 Remove legacy and never used response.Hook 2014-08-29 11:46:11 +02:00
4b9a6305d4 Legacy hooks now need to be explicitely declared 2014-08-28 18:05:21 +02:00
da32ee6490 GitHub module: add command !github_commit 2014-08-28 14:28:56 +02:00
039c578987 New builtin IRC command: !next, similar to !more 2014-08-28 12:43:22 +02:00
97143a0182 Centralize configuration: there is no more XML files for module, juste one bot configuration file, also containing module configuration; fixes #56 2014-08-28 12:29:58 +02:00
fdd4847f71 In config: nick, owner and realname can be overwrited in server node 2014-08-28 12:10:51 +02:00
eae0adbb43 IMDB module: reworking, handle year precision 2014-08-28 02:04:33 +02:00
cd7843e16e Replace help_tiny by module docstring 2014-08-28 01:39:31 +02:00
e5ec487d29 Improve logging system 2014-08-27 07:57:00 +02:00
84d3ee262c IMDB module: detect IMDBid 2014-08-26 16:38:30 +02:00
0e5562fa01 New module: github 2014-08-26 07:06:23 +02:00
2100afed66 Remove 0x01 of CTCP messages only one time, even if parse_content is called multiple time 2014-08-25 12:06:21 +02:00
82156543aa Birthday module: fixes #63 2014-08-21 15:16:25 +02:00
5559ae20d9 Merge pull request #62 from nbr23/v3.4
Added imdb id support as a fallback to imdb title checkout feature.
2014-08-20 18:03:00 +02:00
Max
17c29e386c Added imdb id support as a fallback to imdb title checkout feature. 2014-08-18 17:49:37 +02:00
0a96627d6a Weather module use mapquest module to found city location 2014-08-16 01:26:45 +02:00
46c8048b53 New mapquest module: can geocode (will help #45) 2014-08-16 01:26:05 +02:00
d0b1336d07 Use a logger 2014-08-14 12:50:19 +02:00
3839455f42 Prepare server to incoming split 2014-08-13 17:11:33 +02:00
85981b4d99 Remove dead code: credits 2014-08-13 17:04:11 +02:00
94a9a9a30b Switch to v3.4 branch 2014-08-13 15:53:55 +02:00
ef50f6a1c9 Birthday module: fix regexp string used 2014-08-13 15:25:29 +02:00
fe0f120038 Using newly added Python decorator for hook registration 2014-08-13 15:14:41 +02:00
23bc61cce0 Can register hooks thanks to Python decorator; fixes #43 2014-08-12 20:08:55 +02:00
1464f92c87 More explicit module unloading messages 2014-08-12 17:51:37 +02:00
582da6746f Clean importer (following PEP8); print function in module now handles many argument as Python print 2014-08-12 17:47:42 +02:00
ccff1c8b1e Birthday module: dusting 2014-08-11 14:55:25 +02:00
3ae01da380 Events module: dusting 2014-08-08 19:45:41 +02:00
7aaa65d4a6 XMLnode: when no index, look for attribute on in keyword use 2014-08-08 19:09:18 +02:00
22b6392cc8 Alias module: can create of aliases; fixes #27 2014-08-08 19:07:55 +02:00
1e139b3afa Translate module: fix order of meanings 2014-08-08 18:22:14 +02:00
99c6a5c271 Start a message by nemubot: is equivalent to talk in private message 2014-08-07 19:15:14 +02:00
c15127feb8 WatchWebsite module: dusting & fix unwatch authorizations 2014-08-06 16:20:51 +02:00
d985b71373 Rnd module: fix random choice between 0 possibility 2014-08-06 16:10:10 +02:00
7a55840aeb Translate module: add indication of more available translation 2014-08-06 16:10:04 +02:00
b34a73cea6 Fix exception on empty message 2014-08-04 02:32:29 +02:00
eccf4ddf7a Translation module: allow only translation to or from english (due to wordreference restrinctions) 2014-07-25 18:10:23 +02:00
5bec50744c Dusting some modules 2014-07-25 18:02:30 +02:00
495c1f0efa Refactor translation module 2014-07-25 17:55:49 +02:00
1a3912cc4f YCC module: dusting & anchors are correctly passed; fixes #59 2014-07-25 16:38:01 +02:00
d16f57f8d5 Refresh old code and add antonyme search 2014-07-25 15:12:37 +02:00
9b010544b5 Last PR modules: made some stabilization modifications 2014-07-25 12:53:07 +02:00
d575c0d6d3 tools.web: Avoid explicit redirection loop 2014-07-25 12:52:10 +02:00
6976846e23 Merge pull request #58 from nbr23/v3.3
Added SAP tcode lookup module
2014-07-24 05:37:00 +02:00
Max
be80e84323 Added SAP tcode lookup module 2014-07-24 05:32:07 +02:00
411d8cdc41 New prompt command: netstat that display information about connected networkbot 2014-07-23 16:32:42 +02:00
0036dcc285 Merge pull request #57 from Cccompany/v3.3
IMDB module (fixes  #11) + clean conjugaison module
2014-07-23 15:13:04 +02:00
Bertrand Cournaud
9eb3357148 Clean conjugaison module 2014-07-23 15:01:14 +02:00
Bertrand Cournaud
cd16bf8de9 Update conjugaison module by adding tens 2014-07-23 15:00:01 +02:00
Bertrand Cournaud
fee11aca42 Add imdb search module. \!imdb for info about a movie, \!imdbs to search a movie 2014-07-23 14:54:00 +02:00
fabdbc7c47 v3.3 now considered stable 2014-07-17 12:57:30 +02:00
82198160fd Worlcup module: add a timeout to urlopen to avoid infinite event 2014-07-17 12:04:34 +02:00
ba1b5774bb Too long close is identified 2014-07-17 12:02:16 +02:00
a8ce37a372 Cleaner consumers 2014-07-17 12:01:04 +02:00
e5741ce1cb cmd_server: new command top to display bot load 2014-07-17 11:58:59 +02:00
Bertrand Cournaud
fde217dfde Define a dictonary for the tens list 2014-07-15 11:28:38 +02:00
5555a71ecc Merge pull request #54 from Cccompany/v3.3
That fixes issue #51.
2014-07-11 16:56:08 +02:00
0f01d28528 Webtool: New function to decode htmlentities 2014-07-11 16:37:06 +02:00
Bertrand Cournaud
f6d4600df3 Add conjugaison module 2014-07-11 15:38:30 +02:00
509e85f55a SMS module: after reload, datas are not correctly typed 2014-07-10 23:38:00 +02:00
85edb9489d Choice module: append nick to avoid response stating with a command 2014-07-10 23:35:41 +02:00
de85344b84 Refresh sample configuration files 2014-07-09 15:27:50 +02:00
75def22e57 W3C module: handle document wide messages 2014-07-09 15:19:32 +02:00
4e1fc0bca1 networking module: new feature curly; closes #41 2014-07-08 03:28:01 +02:00
95ceeeaec9 networking module: new feature: w3m to dump page instead of displaying raw HTML 2014-07-08 03:03:33 +02:00
fa77a3b323 Fix decoding of some pages 2014-07-08 02:44:20 +02:00
63f24c7b59 New module worldcup 2014-07-05 02:27:22 +02:00
971e3c882d Fix wiktionary display 2014-07-04 02:13:26 +02:00
bead2e31b2 Fix stacktrace on ACTION CTCP 2014-06-30 10:59:56 +02:00
3e014d4ebe Alias: add commands to list aliases and variables; fixes #49 2014-06-30 10:59:25 +02:00
c5ee6084c0 Webtool: if charset contain also language, ignore language 2014-06-26 23:32:25 +02:00
05a148fef4 Web tool now handles HTTPS connections, content is decoded following given charset header 2014-06-24 18:21:19 +02:00
7575c41645 New module SMS using new Free Mobile API 2014-06-14 00:12:45 +02:00
31fe146c6a Fix !eventslist action, closes #44 2014-06-06 16:18:31 +02:00
f4edaa3c38 XML node: can create an temporary index for instant use 2014-06-06 16:16:50 +02:00
6755b88229 Fix spell module with UTF-8 chars; fixes #21 2014-05-27 16:24:45 +02:00
f97bd593af Fix bug when no module configuration is present 2014-05-27 16:06:47 +02:00
f8dbb7d2e1 Send directly a PONG instead of doing normal treat 2014-05-27 16:05:26 +02:00
be1beec980 Networking module: fix regression on traceurl command 2014-05-05 20:05:58 +02:00
8a3037d288 Weather module: add a command to display stored city coordinates; fixes #40 2014-05-05 18:45:13 +02:00
0cceb8226d Networking module: display error message when API returns error 2014-05-05 18:25:33 +02:00
02d0d91df9 Networking module: avoid error on unexiststant registrant, administrativeContact or technicalContact 2014-05-05 17:52:55 +02:00
34188c71a5 Networking module: Add W3C validator; fixes #4 2014-05-05 12:12:19 +02:00
d549d91aca Networking module: add Whois command; related to issue #4 2014-05-05 11:39:04 +02:00
14e2c59064 Weather module: localize time with timezone; fixes #35 2014-05-05 10:40:07 +02:00
da84a493ed Networking module: add user-agent on isup request; fixes #39 2014-05-05 09:55:47 +02:00
f8999d1e7f Error during save are now skipped and display 2014-05-03 01:25:32 +02:00
aa285b1393 Event module: remove vacs command 2014-05-03 01:06:37 +02:00
a0fca91d06 Reddit module: Show url in response 2014-05-02 00:27:42 +02:00
818d8b754c New module: reddit that display information about a subreddit; close #37 2014-05-01 23:44:26 +02:00
471e40aed1 YCC module: never fail, even if bad URL is given 2014-05-01 23:36:21 +02:00
d56f873fd4 New feature: can now connect to SSL servers (using TLS v1) 2014-05-01 02:34:43 +02:00
b925cee08a New module: weather; close #7 2014-04-30 22:57:28 +02:00
26502abe35 New exception: IRCException
When raised in a module, respond in the channel with the given string as Response content
2014-04-30 22:19:32 +02:00
d38ebd372c DDG module: add examples on !ud commands 2014-04-22 17:23:03 +02:00
08f3a31e88 DDG module: new !urbandictionnary command that display results from Urban Dictionnary website; closes #29 2014-04-22 17:19:10 +02:00
eefcf96516 DDG module: display redirect link on bang request 2014-04-22 17:00:47 +02:00
194 changed files with 13174 additions and 7534 deletions

26
.drone.yml Normal file
View file

@ -0,0 +1,26 @@
---
kind: pipeline
type: docker
name: default-arm64
platform:
os: linux
arch: arm64
steps:
- name: build
image: python:3.11-alpine
commands:
- pip install --no-cache-dir -r requirements.txt
- pip install .
- name: docker
image: plugins/docker
settings:
repo: nemunaire/nemubot
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
username:
from_secret: docker_username
password:
from_secret: docker_password

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
*#
*~
*.log
TAGS
*.py[cod]
__pycache__

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "modules/nextstop/external"]
path = modules/nextstop/external
url = git://github.com/nbr23/NextStop.git

12
.travis.yml Normal file
View file

@ -0,0 +1,12 @@
language: python
python:
- 3.4
- 3.5
- 3.6
- 3.7
- nightly
install:
- pip install -r requirements.txt
- pip install .
script: nosetests -w nemubot
sudo: false

241
DCC.py
View file

@ -1,241 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import imp
import os
import re
import socket
import sys
import time
import threading
import traceback
import message
import server
#Store all used ports
PORTS = list()
class DCC(server.Server):
def __init__(self, srv, dest, socket=None):
server.Server.__init__(self)
self.error = False # An error has occur, closing the connection?
self.messages = list() # Message queued before connexion
# Informations about the sender
self.sender = dest
if self.sender is not None:
self.nick = (self.sender.split('!'))[0]
if self.nick != self.sender:
self.realname = (self.sender.split('!'))[1]
else:
self.realname = self.nick
# Keep the server
self.srv = srv
self.treatement = self.treat_msg
# Found a port for the connection
self.port = self.foundPort()
if self.port is None:
print ("No more available slot for DCC connection")
self.setError("Il n'y a plus de place disponible sur le serveur"
" pour initialiser une session DCC.")
def foundPort(self):
"""Found a free port for the connection"""
for p in range(65432, 65535):
if p not in PORTS:
PORTS.append(p)
return p
return None
@property
def id(self):
"""Gives the server identifiant"""
return self.srv.id + "/" + self.sender
def setError(self, msg):
self.error = True
self.srv.send_msg_usr(self.sender, msg)
def accept_user(self, host, port):
"""Accept a DCC connection"""
self.s = socket.socket()
try:
self.s.connect((host, port))
print ('Accepted user from', host, port, "for", self.sender)
self.connected = True
self.stop = False
except:
self.connected = False
self.error = True
return False
self.start()
return True
def request_user(self, type="CHAT", filename="CHAT", size=""):
"""Create a DCC connection"""
#Open the port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(('', self.port))
except:
try:
self.port = self.foundPort()
s.bind(('', self.port))
except:
self.setError("Une erreur s'est produite durant la tentative"
" d'ouverture d'une session DCC.")
return False
print ('Listen on', self.port, "for", self.sender)
#Send CTCP request for DCC
self.srv.send_ctcp(self.sender,
"DCC %s %s %d %d %s" % (type, filename, self.srv.ip,
self.port, size),
"PRIVMSG")
s.listen(1)
#Waiting for the client
(self.s, addr) = s.accept()
print ('Connected by', addr)
self.connected = True
return True
def send_dcc_raw(self, line):
self.s.sendall(line + b'\n')
def send_dcc(self, msg, to = None):
"""If we talk to this user, send a message through this connection
else, send the message to the server class"""
if to is None or to == self.sender or to == self.nick:
if self.error:
self.srv.send_msg_final(self.nick, msg)
elif not self.connected or self.s is None:
try:
self.start()
except RuntimeError:
pass
self.messages.append(msg)
else:
for line in msg.split("\n"):
self.send_dcc_raw(line.encode())
else:
self.srv.send_dcc(msg, to)
def send_file(self, filename):
"""Send a file over DCC"""
if os.path.isfile(filename):
self.messages = filename
try:
self.start()
except RuntimeError:
pass
else:
print("File not found `%s'" % filename)
def run(self):
self.stopping.clear()
# Send file connection
if not isinstance(self.messages, list):
self.request_user("SEND",
os.path.basename(self.messages),
os.path.getsize(self.messages))
if self.connected:
with open(self.messages, 'rb') as f:
d = f.read(268435456) #Packets size: 256Mo
while d:
self.s.sendall(d)
self.s.recv(4) #The client send a confirmation after each packet
d = f.read(268435456) #Packets size: 256Mo
# Messages connection
else:
if not self.connected:
if not self.request_user():
#TODO: do something here
return False
#Start by sending all queued messages
for mess in self.messages:
self.send_dcc(mess)
time.sleep(1)
readbuffer = b''
self.nicksize = len(self.srv.nick)
self.Bnick = self.srv.nick.encode()
while not self.stop:
raw = self.s.recv(1024) #recieve server messages
if not raw:
break
readbuffer = readbuffer + raw
temp = readbuffer.split(b'\n')
readbuffer = temp.pop()
for line in temp:
self.treatement(line)
if self.connected:
self.s.close()
self.connected = False
#Remove from DCC connections server list
if self.realname in self.srv.dcc_clients:
del self.srv.dcc_clients[self.realname]
print ("Closing connection with", self.nick)
self.stopping.set()
if self.closing_event is not None:
self.closing_event()
#Rearm Thread
threading.Thread.__init__(self)
def treat_msg(self, line):
"""Treat a receive message, *can be overwritten*"""
if line == b'NEMUBOT###':
bot = self.srv.add_networkbot(self.srv, self.sender, self)
self.treatement = bot.treat_msg
self.send_dcc("NEMUBOT###")
elif (line[:self.nicksize] == self.Bnick and
line[self.nicksize+1:].strip()[:10] == b'my name is'):
name = line[self.nicksize+1:].strip()[11:].decode('utf-8',
'replace')
if re.match("^[a-zA-Z0-9_-]+$", name):
if name not in self.srv.dcc_clients:
del self.srv.dcc_clients[self.sender]
self.nick = name
self.sender = self.nick + "!" + self.realname
self.srv.dcc_clients[self.realname] = self
self.send_dcc("Hi " + self.nick)
else:
self.send_dcc("This nickname is already in use"
", please choose another one.")
else:
self.send_dcc("The name you entered contain"
" invalid char.")
else:
self.srv.treat_msg(
(":%s PRIVMSG %s :" % (
self.sender,self.srv.nick)).encode() + line,
True)

21
Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM python:3.11-alpine
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr py3-matrix-nio && \
pip install --no-cache-dir --ignore-installed -r requirements.txt && \
pip install bs4 capstone dnspython openai && \
apk del build-base capstone-dev && \
ln -s /var/lib/nemubot/home /home/nemubot
VOLUME /var/lib/nemubot
COPY . /usr/src/app/
RUN ./setup.py install
WORKDIR /var/lib/nemubot
USER guest
ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-P", "", "-M", "/usr/src/app/modules" ]
CMD [ "-D", "/var/lib/nemubot" ]

View file

@ -1,290 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import errno
import os
import socket
import threading
import traceback
from channel import Channel
from DCC import DCC
from hooks import Hook
import message
import server
import xmlparser
class IRCServer(server.Server):
"""Class to interact with an IRC server"""
def __init__(self, node, nick, owner, realname):
"""Initialize an IRC server
Arguments:
node -- server node from XML configuration
nick -- nick used by the bot on this server
owner -- nick used by the bot owner on this server
realname -- string used as realname on this server
"""
server.Server.__init__(self)
self.node = node
self.nick = nick
self.owner = owner
self.realname = realname
# Listen private messages?
self.listen_nick = True
self.dcc_clients = dict()
self.channels = dict()
for chn in self.node.getNodes("channel"):
chan = Channel(chn["name"], chn["password"])
self.channels[chan.name] = chan
@property
def host(self):
"""Return the server hostname"""
if self.node is not None and self.node.hasAttribute("server"):
return self.node["server"]
else:
return "localhost"
@property
def port(self):
"""Return the connection port used on this server"""
if self.node is not None and self.node.hasAttribute("port"):
return self.node.getInt("port")
else:
return "6667"
@property
def password(self):
"""Return the password used to connect to this server"""
if self.node is not None and self.node.hasAttribute("password"):
return self.node["password"]
else:
return None
@property
def allow_all(self):
"""If True, treat message from all channels, not only listed one"""
return (self.node is not None and self.node.hasAttribute("allowall")
and self.node["allowall"] == "true")
@property
def autoconnect(self):
"""Autoconnect the server when added"""
if self.node is not None and self.node.hasAttribute("autoconnect"):
value = self.node["autoconnect"].lower()
return value != "no" and value != "off" and value != "false"
else:
return False
@property
def id(self):
"""Gives the server identifiant"""
return self.host + ":" + str(self.port)
def register_hooks(self):
self.add_hook(Hook(self.evt_channel, "JOIN"))
self.add_hook(Hook(self.evt_channel, "PART"))
self.add_hook(Hook(self.evt_server, "NICK"))
self.add_hook(Hook(self.evt_server, "QUIT"))
self.add_hook(Hook(self.evt_channel, "332"))
self.add_hook(Hook(self.evt_channel, "353"))
def evt_server(self, msg, srv):
for chan in self.channels:
self.channels[chan].treat(msg.cmd, msg)
def evt_channel(self, msg, srv):
if msg.channel is not None:
if msg.channel in self.channels:
self.channels[msg.channel].treat(msg.cmd, msg)
def accepted_channel(self, chan, sender=None):
"""Return True if the channel (or the user) is authorized"""
if self.allow_all:
return True
elif self.listen_nick:
return (chan in self.channels and (sender is None or sender in
self.channels[chan].people)
) or chan == self.nick
else:
return chan in self.channels and (sender is None or sender
in self.channels[chan].people)
def join(self, chan, password=None, force=False):
"""Join a channel"""
if force or (chan is not None and
self.connected and chan not in self.channels):
self.channels[chan] = Channel(chan, password)
if password is not None:
self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode())
else:
self.s.send(("JOIN %s\r\n" % chan).encode())
return True
else:
return False
def leave(self, chan):
"""Leave a channel"""
if chan is not None and self.connected and chan in self.channels:
if isinstance(chan, list):
for c in chan:
self.leave(c)
else:
self.s.send(("PART %s\r\n" % self.channels[chan].name).encode())
del self.channels[chan]
return True
else:
return False
# Main loop
def run(self):
if not self.connected:
self.s = socket.socket() #Create the socket
try:
self.s.connect((self.host, self.port)) #Connect to server
except socket.error as e:
self.s = None
print ("\033[1;31mError:\033[0m Unable to connect to %s:%d: %s"
% (self.host, self.port, os.strerror(e.errno)))
return
self.stopping.clear()
if self.password != None:
self.s.send(b"PASS " + self.password.encode () + b"\r\n")
self.s.send(("NICK %s\r\n" % self.nick).encode ())
self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host,
self.realname)).encode())
raw = self.s.recv(1024)
if not raw:
print ("Unable to connect to %s:%d" % (self.host, self.port))
return
self.connected = True
print ("Connection to %s:%d completed" % (self.host, self.port))
if len(self.channels) > 0:
for chn in self.channels.keys():
self.join(self.channels[chn].name,
self.channels[chn].password, force=True)
readbuffer = b'' #Here we store all the messages from server
try:
while not self.stop:
readbuffer = readbuffer + raw
temp = readbuffer.split(b'\n')
readbuffer = temp.pop()
for line in temp:
self.treat_msg(line)
raw = self.s.recv(1024) #recieve server messages
except socket.error:
pass
if self.connected:
self.s.close()
self.connected = False
if self.closing_event is not None:
self.closing_event()
print ("Server `%s' successfully stopped." % self.id)
self.stopping.set()
# Rearm Thread
threading.Thread.__init__(self)
# Overwritted methods
def disconnect(self):
"""Close the socket with the server and all DCC client connections"""
#Close all DCC connection
clts = [c for c in self.dcc_clients]
for clt in clts:
self.dcc_clients[clt].disconnect()
return server.Server.disconnect(self)
# Abstract methods
def send_pong(self, cnt):
"""Send a PONG command to the server with argument cnt"""
self.s.send(("PONG %s\r\n" % cnt).encode())
def msg_treated(self, origin):
"""Do nothing; here for implement abstract class"""
pass
def send_dcc(self, msg, to):
"""Send a message through DCC connection"""
if msg is not None and to is not None:
realname = to.split("!")[1]
if realname not in self.dcc_clients.keys():
d = DCC(self, to)
self.dcc_clients[realname] = d
self.dcc_clients[realname].send_dcc(msg)
def send_msg_final(self, channel, line, cmd="PRIVMSG", endl="\r\n"):
"""Send a message without checks or format"""
#TODO: add something for post message treatment here
if channel == self.nick:
print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg)
traceback.print_stack()
if line is not None and channel is not None:
if self.s is None:
print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, line))
traceback.print_stack()
elif len(line) < 442:
self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ())
else:
print ("\033[1;35mWarning:\033[0m Message truncated due to size (%d ; max : 442) : %s" % (len(line), line))
traceback.print_stack()
self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ())
def send_msg_usr(self, user, msg):
"""Send a message to a user instead of a channel"""
if user is not None and user[0] != "#":
realname = user.split("!")[1]
if realname in self.dcc_clients or user in self.dcc_clients:
self.send_dcc(msg, user)
else:
for line in msg.split("\n"):
if line != "":
self.send_msg_final(user.split('!')[0], msg)
def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
"""Send a message to a channel"""
if self.accepted_channel(channel):
server.Server.send_msg(self, channel, msg, cmd, endl)
def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"):
"""Send a message to a channel, only if the source user is on this channel too"""
if self.accepted_channel(channel, sender):
self.send_msg_final(channel, msg, cmd, endl)
def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"):
"""Send a message to all channels on this server"""
for channel in self.channels.keys():
self.send_msg(channel, msg, cmd, endl)

View file

@ -1,7 +1,50 @@
# *nemubot*
nemubot
=======
An extremely modulable IRC bot, built around XML configuration files!
## Documentation
Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki
Requirements
------------
*nemubot* requires at least Python 3.3 to work.
Some modules (like `cve`, `nextstop` or `laposte`) require the
[BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/),
but the core and framework has no dependency.
Installation
------------
Use the `setup.py` file: `python setup.py install`.
### VirtualEnv setup
The easiest way to do this is through a virtualenv:
```sh
virtualenv venv
. venv/bin/activate
python setup.py install
```
### Create a new configuration file
There is a sample configuration file, called `bot_sample.xml`. You can
create your own configuration file from it.
Usage
-----
Don't forget to activate your virtualenv in further terminals, if you
use it.
To launch the bot, run:
```sh
nemubot bot.xml
```
Where `bot.xml` is your configuration file.

24
bin/nemubot Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
from nemubot.__main__ import main
if __name__ == "__main__":
main()

641
bot.py
View file

@ -1,641 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from datetime import timedelta
from queue import Queue
import threading
import time
import re
import consumer
import event
import hooks
from networkbot import NetworkBot
from IRCServer import IRCServer
from DCC import DCC
import response
ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
class Bot:
def __init__(self, ip, realname, mp=list()):
# Bot general informations
self.version = 3.3
self.version_txt = "3.3-dev"
# Save various informations
self.ip = ip
self.realname = realname
self.ctcp_capabilities = dict()
self.init_ctcp_capabilities()
# Keep global context: servers and modules
self.servers = dict()
self.modules = dict()
# Context paths
self.modules_path = mp
self.datas_path = './datas/'
# Events
self.events = list()
self.event_timer = None
# Own hooks
self.hooks = hooks.MessagesHook(self, self)
# Other known bots, making a bots network
self.network = dict()
self.hooks_cache = dict()
# Messages to be treated
self.cnsr_queue = Queue()
self.cnsr_thrd = list()
self.cnsr_thrd_size = -1
self.hooks.add_hook("irc_hook",
hooks.Hook(self.treat_prvmsg, "PRIVMSG"),
self)
def init_ctcp_capabilities(self):
"""Reset existing CTCP capabilities to default one"""
self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive")
self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo
self.ctcp_capabilities["DCC"] = self._ctcp_dcc
self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response(
msg.sender, "NEMUBOT %f" % self.version)
self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response(
msg.sender, "TIME %s" % (datetime.now()))
self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response(
msg.sender, "USERINFO %s" % self.realname)
self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response(
msg.sender, "VERSION nemubot v%s" % self.version_txt)
def _ctcp_clientinfo(self, srv, msg):
"""Response to CLIENTINFO CTCP message"""
return _ctcp_response(msg.sndr,
" ".join(self.ctcp_capabilities.keys()))
def _ctcp_dcc(self, srv, msg):
"""Response to DCC CTCP message"""
ip = srv.toIP(int(msg.cmds[3]))
conn = DCC(srv, msg.sender)
if conn.accept_user(ip, int(msg.cmds[4])):
srv.dcc_clients[conn.sender] = conn
conn.send_dcc("Hello %s!" % conn.nick)
else:
print ("DCC: unable to connect to %s:%s" % (ip, msg.cmds[4]))
def add_event(self, evt, eid=None, module_src=None):
"""Register an event and return its identifiant for futur update"""
if eid is None:
# Find an ID
now = datetime.now()
evt.id = "%d%c%d%d%c%d%d%c%d" % (now.year, ID_letters[now.microsecond % 52],
now.month, now.day, ID_letters[now.microsecond % 42],
now.hour, now.minute, ID_letters[now.microsecond % 32],
now.second)
else:
evt.id = eid
# Add the event in place
t = evt.current
i = -1
for i in range(0, len(self.events)):
if self.events[i].current > t:
i -= 1
break
self.events.insert(i + 1, evt)
if i == -1:
self.update_timer()
if len(self.events) <= 0 or self.events[i+1] != evt:
return None
if module_src is not None:
module_src.REGISTERED_EVENTS.append(evt.id)
return evt.id
def del_event(self, id, module_src=None):
"""Find and remove an event from list"""
if len(self.events) > 0 and id == self.events[0].id:
self.events.remove(self.events[0])
self.update_timer()
if module_src is not None:
module_src.REGISTERED_EVENTS.remove(evt.id)
return True
for evt in self.events:
if evt.id == id:
self.events.remove(evt)
if module_src is not None:
module_src.REGISTERED_EVENTS.remove(evt.id)
return True
return False
def update_timer(self):
"""Relaunch the timer to end with the closest event"""
# Reset the timer if this is the first item
if self.event_timer is not None:
self.event_timer.cancel()
if len(self.events) > 0:
#print ("Update timer, next in", self.events[0].time_left.seconds,
# "seconds")
if datetime.now() + timedelta(seconds=5) >= self.events[0].current:
while datetime.now() < self.events[0].current:
time.sleep(0.6)
self.end_timer()
else:
self.event_timer = threading.Timer(
self.events[0].time_left.seconds + 1, self.end_timer)
self.event_timer.start()
#else:
# print ("Update timer: no timer left")
def end_timer(self):
"""Function called at the end of the timer"""
#print ("end timer")
while len(self.events)>0 and datetime.now() >= self.events[0].current:
#print ("end timer: while")
evt = self.events.pop(0)
self.cnsr_queue.put_nowait(consumer.EventConsumer(evt))
self.update_consumers()
self.update_timer()
def addServer(self, node, nick, owner, realname):
"""Add a new server to the context"""
srv = IRCServer(node, nick, owner, realname)
srv.add_hook = lambda h: self.hooks.add_hook("irc_hook", h, self)
srv.add_networkbot = self.add_networkbot
srv.send_bot = lambda d: self.send_networkbot(srv, d)
srv.register_hooks()
if srv.id not in self.servers:
self.servers[srv.id] = srv
if srv.autoconnect:
srv.launch(self.receive_message)
return True
else:
return False
def add_module(self, module):
"""Add a module to the context, if already exists, unload the
old one before"""
# Check if the module already exists
for mod in self.modules.keys():
if self.modules[mod].name == module.name:
self.unload_module(self.modules[mod].name)
break
self.modules[module.name] = module
return True
def add_modules_path(self, path):
"""Add a path to the modules_path array, used by module loader"""
# The path must end by / char
if path[len(path)-1] != "/":
path = path + "/"
if path not in self.modules_path:
self.modules_path.append(path)
return True
return False
def unload_module(self, name, verb=False):
"""Unload a module"""
if name in self.modules:
print (name)
self.modules[name].save()
if hasattr(self.modules[name], "unload"):
self.modules[name].unload(self)
# Remove registered hooks
for (s, h) in self.modules[name].REGISTERED_HOOKS:
self.hooks.del_hook(s, h)
# Remove registered events
for e in self.modules[name].REGISTERED_EVENTS:
self.del_event(e)
# Remove from the dict
del self.modules[name]
return True
return False
def update_consumers(self):
"""Launch new consumer thread if necessary"""
if self.cnsr_queue.qsize() > self.cnsr_thrd_size:
c = consumer.Consumer(self)
self.cnsr_thrd.append(c)
c.start()
self.cnsr_thrd_size += 2
def receive_message(self, srv, raw_msg, private=False, data=None):
"""Queued the message for treatment"""
#print (raw_msg)
self.cnsr_queue.put_nowait(consumer.MessageConsumer(srv, raw_msg, datetime.now(), private, data))
# Launch a new thread if necessary
self.update_consumers()
def add_networkbot(self, srv, dest, dcc=None):
"""Append a new bot into the network"""
id = srv.id + "/" + dest
if id not in self.network:
self.network[id] = NetworkBot(self, srv, dest, dcc)
return self.network[id]
def send_networkbot(self, srv, cmd, data=None):
for bot in self.network:
if self.network[bot].srv == srv:
self.network[bot].send_cmd(cmd, data)
def quit(self, verb=False):
"""Save and unload modules and disconnect servers"""
if self.event_timer is not None:
if verb: print ("Stop the event timer...")
self.event_timer.cancel()
if verb: print ("Save and unload all modules...")
k = list(self.modules.keys())
for mod in k:
self.unload_module(mod, verb)
if verb: print ("Close all servers connection...")
k = list(self.servers.keys())
for srv in k:
self.servers[srv].disconnect()
# Hooks cache
def create_cache(self, name):
if name not in self.hooks_cache:
if isinstance(self.hooks.__dict__[name], list):
self.hooks_cache[name] = list()
# Start by adding locals hooks
for h in self.hooks.__dict__[name]:
tpl = (h, 0, self.hooks.__dict__[name], self.hooks.bot)
self.hooks_cache[name].append(tpl)
# Now, add extermal hooks
level = 0
while level == 0 or lvl_exist:
lvl_exist = False
for ext in self.network:
if len(self.network[ext].hooks) > level:
lvl_exist = True
for h in self.network[ext].hooks[level].__dict__[name]:
if h not in self.hooks_cache[name]:
self.hooks_cache[name].append((h, level + 1,
self.network[ext].hooks[level].__dict__[name], self.network[ext].hooks[level].bot))
level += 1
elif isinstance(self.hooks.__dict__[name], dict):
self.hooks_cache[name] = dict()
# Start by adding locals hooks
for h in self.hooks.__dict__[name]:
self.hooks_cache[name][h] = (self.hooks.__dict__[name][h], 0,
self.hooks.__dict__[name],
self.hooks.bot)
# Now, add extermal hooks
level = 0
while level == 0 or lvl_exist:
lvl_exist = False
for ext in self.network:
if len(self.network[ext].hooks) > level:
lvl_exist = True
for h in self.network[ext].hooks[level].__dict__[name]:
if h not in self.hooks_cache[name]:
self.hooks_cache[name][h] = (self.network[ext].hooks[level].__dict__[name][h], level + 1, self.network[ext].hooks[level].__dict__[name], self.network[ext].hooks[level].bot)
level += 1
else:
raise Exception(name + " hook type unrecognized")
return self.hooks_cache[name]
# Treatment
def check_rest_times(self, store, hook):
"""Remove from store the hook if it has been executed given time"""
if hook.times == 0:
if isinstance(store, dict):
store[hook.name].remove(hook)
if len(store) == 0:
del store[hook.name]
elif isinstance(store, list):
store.remove(hook)
def treat_pre(self, msg, srv):
"""Treat a message before all other treatment"""
for h, lvl, store, bot in self.create_cache("all_pre"):
if h.is_matching(None, server=srv):
h.run(msg, self.create_cache)
self.check_rest_times(store, h)
def treat_post(self, res):
"""Treat a message before send"""
for h, lvl, store, bot in self.create_cache("all_post"):
if h.is_matching(None, channel=res.channel, server=res.server):
c = h.run(res)
self.check_rest_times(store, h)
if not c:
return False
return True
def treat_irc(self, msg, srv):
"""Treat all incoming IRC commands"""
treated = list()
irc_hooks = self.create_cache("irc_hook")
if msg.cmd in irc_hooks:
(hks, lvl, store, bot) = irc_hooks[msg.cmd]
for h in hks:
if h.is_matching(msg.cmd, server=srv):
res = h.run(msg, srv, msg.cmd)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, h)
return treated
def treat_prvmsg_ask(self, msg, srv):
# Treat ping
if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
msg.content, re.I) is not None:
return response.Response(msg.sender, message="pong",
channel=msg.channel, nick=msg.nick)
# Ask hooks
else:
return self.treat_ask(msg, srv)
def treat_prvmsg(self, msg, srv):
# First, treat CTCP
if msg.ctcp:
if msg.cmds[0] in self.ctcp_capabilities:
return self.ctcp_capabilities[msg.cmds[0]](srv, msg)
else:
return _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request")
# Treat all messages starting with 'nemubot:' as distinct commands
elif msg.content.find("%s:"%srv.nick) == 0:
# Remove the bot name
msg.content = msg.content[len(srv.nick)+1:].strip()
return self.treat_prvmsg_ask(msg, srv)
# Owner commands
elif msg.content[0] == '`' and msg.nick == srv.owner:
#TODO: owner commands
pass
elif msg.content[0] == '!' and len(msg.content) > 1:
# Remove the !
msg.cmds[0] = msg.cmds[0][1:]
if msg.cmds[0] == "help":
return _help_msg(msg.sender, self.modules, msg.cmds)
elif msg.cmds[0] == "more":
if msg.channel == srv.nick:
if msg.sender in srv.moremessages:
return srv.moremessages[msg.sender]
else:
if msg.channel in srv.moremessages:
return srv.moremessages[msg.channel]
elif msg.cmds[0] == "dcc":
print("dcctest for", msg.sender)
srv.send_dcc("Hello %s!" % msg.nick, msg.sender)
elif msg.cmds[0] == "pvdcctest":
print("dcctest")
return Response(msg.sender, message="Test DCC")
elif msg.cmds[0] == "dccsendtest":
print("dccsendtest")
conn = DCC(srv, msg.sender)
conn.send_file("bot_sample.xml")
else:
return self.treat_cmd(msg, srv)
else:
res = self.treat_answer(msg, srv)
# Assume the message starts with nemubot:
if (res is None or len(res) <= 0) and msg.private:
return self.treat_prvmsg_ask(msg, srv)
return res
def treat_cmd(self, msg, srv):
"""Treat a command message"""
treated = list()
# First, treat simple hook
cmd_hook = self.create_cache("cmd_hook")
if msg.cmds[0] in cmd_hook:
(hks, lvl, store, bot) = cmd_hook[msg.cmds[0]]
for h in hks:
if h.is_matching(msg.cmds[0], channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
res = h.run(msg, strcmp=msg.cmds[0])
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, h)
# Then, treat regexp based hook
cmd_rgxp = self.create_cache("cmd_rgxp")
for hook, lvl, store, bot in cmd_rgxp:
if hook.is_matching(msg.cmds[0], msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
# Finally, treat default hooks if not catched before
cmd_default = self.create_cache("cmd_default")
for hook, lvl, store, bot in cmd_default:
if treated:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
return treated
def treat_ask(self, msg, srv):
"""Treat an ask message"""
treated = list()
# First, treat simple hook
ask_hook = self.create_cache("ask_hook")
if msg.content in ask_hook:
hks, lvl, store, bot = ask_hook[msg.content]
for h in hks:
if h.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
res = h.run(msg, strcmp=msg.content)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, h)
# Then, treat regexp based hook
ask_rgxp = self.create_cache("ask_rgxp")
for hook, lvl, store, bot in ask_rgxp:
if hook.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
res = hook.run(msg, strcmp=msg.content)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
# Finally, treat default hooks if not catched before
ask_default = self.create_cache("ask_default")
for hook, lvl, store, bot in ask_default:
if treated:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
return treated
def treat_answer(self, msg, srv):
"""Treat a normal message"""
treated = list()
# First, treat simple hook
msg_hook = self.create_cache("msg_hook")
if msg.content in msg_hook:
hks, lvl, store, bot = msg_hook[msg.content]
for h in hks:
if h.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
res = h.run(msg, strcmp=msg.content)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, h)
# Then, treat regexp based hook
msg_rgxp = self.create_cache("msg_rgxp")
for hook, lvl, store, bot in msg_rgxp:
if hook.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
res = hook.run(msg, strcmp=msg.content)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
# Finally, treat default hooks if not catched before
msg_default = self.create_cache("msg_default")
for hook, lvl, store, bot in msg_default:
if len(treated) > 0:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
return treated
def _ctcp_response(sndr, msg):
return response.Response(sndr, msg, ctcp=True)
def _help_msg(sndr, modules, cmd):
"""Parse and response to help messages"""
res = response.Response(sndr)
if len(cmd) > 1:
if cmd[1] in modules:
if len(cmd) > 2:
if hasattr(modules[cmd[1]], "HELP_cmd"):
res.append_message(modules[cmd[1]].HELP_cmd(cmd[2]))
else:
res.append_message("No help for command %s in module %s" % (cmd[2], cmd[1]))
elif hasattr(modules[cmd[1]], "help_full"):
res.append_message(modules[cmd[1]].help_full())
else:
res.append_message("No help for module %s" % cmd[1])
else:
res.append_message("No module named %s" % cmd[1])
else:
res.append_message("Pour me demander quelque chose, commencez "
"votre message par mon nom ; je réagis "
"également à certaine commandes commençant par"
" !. Pour plus d'informations, envoyez le "
"message \"!more\".")
res.append_message("Mon code source est libre, publié sous "
"licence AGPL (http://www.gnu.org/licenses/). "
"Vous pouvez le consulter, le dupliquer, "
"envoyer des rapports de bogues ou bien "
"contribuer au projet sur GitHub : "
"http://github.com/nemunaire/nemubot/")
res.append_message(title="Pour plus de détails sur un module, "
"envoyez \"!help nomdumodule\". Voici la liste"
" de tous les modules disponibles localement",
message=["\x03\x02%s\x03\x02 (%s)" % (im, modules[im].help_tiny ()) for im in modules if hasattr(modules[im], "help_tiny")])
return res
def hotswap(bak):
return Bot(bak.servers, bak.modules, bak.modules_path)
def reload():
import imp
import channel
imp.reload(channel)
import consumer
imp.reload(consumer)
import DCC
imp.reload(DCC)
import event
imp.reload(event)
import hooks
imp.reload(hooks)
import importer
imp.reload(importer)
import message
imp.reload(message)
import prompt.builtins
imp.reload(prompt.builtins)
import server
imp.reload(server)
import xmlparser
imp.reload(xmlparser)
import xmlparser.node
imp.reload(xmlparser.node)

View file

@ -1,13 +1,23 @@
<nemubotconfig nick="nemubot" realname="nemubot speaker" owner="someone">
<server server="irc.freenode.org" port="6667" password="secret" autoconnect="true">
<nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone">
<server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso">
<channel name="#nemutest" />
</server>
<load path="modules/birthday.xml" />
<load path="modules/ycc.xml" />
<load path="modules/qcm.xml" />
<load path="modules/soutenance.xml" />
<load path="modules/velib.xml" />
<load path="modules/whereis.xml" />
<load path="modules/watchWebsite.xml" />
<load path="modules/events.xml" />
<!--
<server host="ircs://my_host.local:6667" password="secret" autoconnect="true">
<channel name="#nemutest" />
</server>
-->
<!--
<module name="wolframalpha" apikey="YOUR-APIKEY" />
-->
<module name="cmd_server" />
<module name="alias" />
<module name="ycc" />
<module name="events" />
</nemubotconfig>

View file

@ -1,102 +0,0 @@
# coding=utf-8
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Channel:
def __init__(self, name, password=None):
self.name = name
self.password = password
self.people = dict()
self.topic = ""
def treat(self, cmd, msg):
if cmd == "353":
self.parse353(msg)
elif cmd == "332":
self.parse332(msg)
elif cmd == "MODE":
self.mode(msg)
elif cmd == "JOIN":
self.join(msg.nick)
elif cmd == "NICK":
self.nick(msg.nick, msg.content)
elif cmd == "PART" or cmd == "QUIT":
self.part(msg.nick)
elif cmd == "TOPIC":
self.topic = self.content
def join(self, nick, level = 0):
"""Someone join the channel"""
#print ("%s arrive sur %s" % (nick, self.name))
self.people[nick] = level
def chtopic(self, newtopic):
"""Send command to change the topic"""
self.srv.send_msg(self.name, newtopic, "TOPIC")
self.topic = newtopic
def nick(self, oldnick, newnick):
"""Someone change his nick"""
if oldnick in self.people:
#print ("%s change de nom pour %s sur %s" % (oldnick, newnick, self.name))
lvl = self.people[oldnick]
del self.people[oldnick]
self.people[newnick] = lvl
def part(self, nick):
"""Someone leave the channel"""
if nick in self.people:
#print ("%s vient de quitter %s" % (nick, self.name))
del self.people[nick]
def mode(self, msg):
if msg.content[0] == "-k":
self.password = ""
elif msg.content[0] == "+k":
if len(msg.content) > 1:
self.password = ' '.join(msg.content[1:])[1:]
else:
self.password = msg.content[1]
elif msg.content[0] == "+o":
self.people[msg.nick] |= 4
elif msg.content[0] == "-o":
self.people[msg.nick] &= ~4
elif msg.content[0] == "+h":
self.people[msg.nick] |= 2
elif msg.content[0] == "-h":
self.people[msg.nick] &= ~2
elif msg.content[0] == "+v":
self.people[msg.nick] |= 1
elif msg.content[0] == "-v":
self.people[msg.nick] &= ~1
def parse332(self, msg):
self.topic = msg.content
def parse353(self, msg):
for p in msg.content:
p = p.decode()
if p[0] == "@":
level = 4
elif p[0] == "%":
level = 2
elif p[0] == "+":
level = 1
else:
self.join(p, 0)
continue
self.join(p[1:], level)

View file

@ -1,143 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import queue
import re
import threading
import traceback
import sys
import bot
from DCC import DCC
from message import Message
import response
import server
class MessageConsumer:
"""Store a message before treating"""
def __init__(self, srv, raw, time, prvt, data):
self.srv = srv
self.raw = raw
self.time = time
self.prvt = prvt
self.data = data
def treat_in(self, context, msg):
"""Treat the input message"""
if msg.cmd == "PING":
self.srv.send_pong(msg.content)
else:
# TODO: Manage credits
if msg.channel is None or self.srv.accepted_channel(msg.channel):
# All messages
context.treat_pre(msg, self.srv)
return context.treat_irc(msg, self.srv)
def treat_out(self, context, res):
"""Treat the output message"""
if isinstance(res, list):
for r in res:
if r is not None: self.treat_out(context, r)
elif isinstance(res, response.Response):
# Define the destination server
if (res.server is not None and
isinstance(res.server, str) and res.server in context.servers):
res.server = context.servers[res.server]
if (res.server is not None and
not isinstance(res.server, server.Server)):
print ("\033[1;35mWarning:\033[0m the server defined in this "
"response doesn't exist: %s" % (res.server))
res.server = None
if res.server is None:
res.server = self.srv
# Sent the message only if treat_post authorize it
if context.treat_post(res):
res.server.send_response(res, self.data)
elif isinstance(res, response.Hook):
context.hooks.add_hook(res.type, res.hook, res.src)
elif res is not None:
print ("\033[1;35mWarning:\033[0m unrecognized response type "
": %s" % res)
def run(self, context):
"""Create, parse and treat the message"""
try:
msg = Message(self.raw, self.time, self.prvt)
msg.server = self.srv.id
if msg.cmd == "PRIVMSG":
msg.is_owner = (msg.nick == self.srv.owner)
res = self.treat_in(context, msg)
except:
print ("\033[1;31mERROR:\033[0m occurred during the "
"processing of the message: %s" % self.raw)
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value,
exc_traceback)
return
# Send message
self.treat_out(context, res)
# Inform that the message has been treated
self.srv.msg_treated(self.data)
class EventConsumer:
"""Store a event before treating"""
def __init__(self, evt, timeout=20):
self.evt = evt
self.timeout = timeout
def run(self, context):
try:
self.evt.launch_check()
except:
print ("\033[1;31mError:\033[0m during event end")
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value,
exc_traceback)
if self.evt.next is not None:
context.add_event(self.evt, self.evt.id)
class Consumer(threading.Thread):
"""Dequeue and exec requested action"""
def __init__(self, context):
self.context = context
self.stop = False
threading.Thread.__init__(self)
def run(self):
try:
while not self.stop:
stm = self.context.cnsr_queue.get(True, 20)
stm.run(self.context)
except queue.Empty:
pass
finally:
self.context.cnsr_thrd_size -= 2

View file

@ -1,43 +0,0 @@
# coding=utf-8
from datetime import datetime
from datetime import timedelta
import random
BANLIST = []
class Credits:
def __init__ (self, name):
self.name = name
self.credits = 5
self.randsec = timedelta(seconds=random.randint(0, 55))
self.lastmessage = datetime.now() + self.randsec
self.iask = True
def ask(self):
if self.name in BANLIST:
return False
now = datetime.now() + self.randsec
if self.lastmessage.minute == now.minute and (self.lastmessage.second == now.second or self.lastmessage.second == now.second - 1):
print("\033[1;36mAUTOBAN\033[0m %s: too low time between messages" % self.name)
#BANLIST.append(self.name)
self.credits -= self.credits / 2 #Une alternative
return False
self.iask = True
return self.credits > 0 or self.lastmessage.minute != now.minute
def speak(self):
if self.iask:
self.iask = False
now = datetime.now() + self.randsec
if self.lastmessage.minute != now.minute:
self.credits = min (15, self.credits + 5)
self.lastmessage = now
self.credits -= 1
return self.credits > -3
def to_string(self):
print ("%s: %d ; reset: %d" % (self.name, self.credits, self.randsec.seconds))

View file

118
event.py
View file

@ -1,118 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from datetime import timedelta
class ModuleEvent:
def __init__(self, func=None, func_data=None, check=None, cmp_data=None,
intervalle=60, offset=0, call=None, call_data=None, times=1):
# What have we to check?
self.func = func
self.func_data = func_data
# How detect a change?
self.check = check
if cmp_data is not None:
self.cmp_data = cmp_data
elif self.func is not None:
if self.func_data is None:
self.cmp_data = self.func()
elif isinstance(self.func_data, dict):
self.cmp_data = self.func(**self.func_data)
else:
self.cmp_data = self.func(self.func_data)
else:
self.cmp_data = None
self.offset = timedelta(seconds=offset) # Time to wait before the first check
self.intervalle = timedelta(seconds=intervalle)
self.end = None
# What should we call when
self.call = call
if call_data is not None:
self.call_data = call_data
else:
self.call_data = func_data
# How many times do this event?
self.times = times
@property
def current(self):
"""Return the date of the near check"""
if self.times != 0:
if self.end is None:
self.end = datetime.now() + self.offset + self.intervalle
return self.end
return None
@property
def next(self):
"""Return the date of the next check"""
if self.times != 0:
if self.end is None:
return self.current
elif self.end < datetime.now():
self.end += self.intervalle
return self.end
return None
@property
def time_left(self):
"""Return the time left before/after the near check"""
if self.current is not None:
return self.current - datetime.now()
return 99999
def launch_check(self):
if self.func is None:
d = self.func_data
elif self.func_data is None:
d = self.func()
elif isinstance(self.func_data, dict):
d = self.func(**self.func_data)
else:
d = self.func(self.func_data)
#print ("do test with", d, self.cmp_data)
if self.check is None:
if self.cmp_data is None:
r = True
else:
r = d != self.cmp_data
elif self.cmp_data is None:
r = self.check(d)
elif isinstance(self.cmp_data, dict):
r = self.check(d, **self.cmp_data)
else:
r = self.check(d, self.cmp_data)
if r:
self.times -= 1
if self.call_data is None:
if d is None:
self.call()
else:
self.call(d)
elif isinstance(self.call_data, dict):
self.call(d, **self.call_data)
else:
self.call(d, self.call_data)

220
hooks.py
View file

@ -1,220 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from response import Response
class MessagesHook:
def __init__(self, context, bot):
self.context = context
self.bot = bot
# Store specials hooks
self.all_pre = list() # Treated before any parse
self.all_post = list() # Treated before send message to user
# Store IRC commands hooks
self.irc_hook = dict()
# Store direct hooks
self.cmd_hook = dict()
self.ask_hook = dict()
self.msg_hook = dict()
# Store regexp hooks
self.cmd_rgxp = list()
self.ask_rgxp = list()
self.msg_rgxp = list()
# Store default hooks (after other hooks if no match)
self.cmd_default = list()
self.ask_default = list()
self.msg_default = list()
def add_hook(self, store, hook, module_src=None):
"""Insert in the right place a hook into the given store"""
if module_src is None:
print ("\033[1;35mWarning:\033[0m No source module was passed to "
"add_hook function, please fix it in order to be "
"compatible with unload feature")
if store in self.context.hooks_cache:
del self.context.hooks_cache[store]
if not hasattr(self, store):
print ("\033[1;35mWarning:\033[0m unrecognized hook store")
return
attr = getattr(self, store)
if isinstance(attr, dict) and hook.name is not None:
if hook.name not in attr:
attr[hook.name] = list()
attr[hook.name].append(hook)
if hook.end is not None:
if hook.end not in attr:
attr[hook.end] = list()
attr[hook.end].append(hook)
elif isinstance(attr, list):
attr.append(hook)
else:
print ("\033[1;32mWarning:\033[0m unrecognized hook store type")
return
if module_src is not None and hasattr(module_src, "REGISTERED_HOOKS"):
module_src.REGISTERED_HOOKS.append((store, hook))
def register_hook_attributes(self, store, module, node):
if node.hasAttribute("data"):
data = node["data"]
else:
data = None
if node.hasAttribute("name"):
self.add_hook(store + "_hook", Hook(getattr(module, node["call"]),
node["name"], data=data),
module)
elif node.hasAttribute("regexp"):
self.add_hook(store + "_rgxp", Hook(getattr(module, node["call"]),
regexp=node["regexp"], data=data),
module)
def register_hook(self, module, node):
"""Create a hook from configuration node"""
if node.name == "message" and node.hasAttribute("type"):
if node["type"] == "cmd" or node["type"] == "all":
self.register_hook_attributes("cmd", module, node)
if node["type"] == "ask" or node["type"] == "all":
self.register_hook_attributes("ask", module, node)
if (node["type"] == "msg" or node["type"] == "answer" or
node["type"] == "all"):
self.register_hook_attributes("answer", module, node)
def clear(self):
for h in self.all_pre:
self.del_hook("all_pre", h)
for h in self.all_post:
self.del_hook("all_post", h)
for l in self.irc_hook:
for h in self.irc_hook[l]:
self.del_hook("irc_hook", h)
for l in self.cmd_hook:
for h in self.cmd_hook[l]:
self.del_hook("cmd_hook", h)
for l in self.ask_hook:
for h in self.ask_hook[l]:
self.del_hook("ask_hook", h)
for l in self.msg_hook:
for h in self.msg_hook[l]:
self.del_hook("msg_hook", h)
for h in self.cmd_rgxp:
self.del_hook("cmd_rgxp", h)
for h in self.ask_rgxp:
self.del_hook("ask_rgxp", h)
for h in self.msg_rgxp:
self.del_hook("msg_rgxp", h)
for h in self.cmd_default:
self.del_hook("cmd_default", h)
for h in self.ask_default:
self.del_hook("ask_default", h)
for h in self.msg_default:
self.del_hook("msg_default", h)
def del_hook(self, store, hook, module_src=None):
"""Remove a registered hook from a given store"""
if store in self.context.hooks_cache:
del self.context.hooks_cache[store]
if not hasattr(self, store):
print ("Warning: unrecognized hook store type")
return
attr = getattr(self, store)
if isinstance(attr, dict) and hook.name is not None:
if hook.name in attr:
attr[hook.name].remove(hook)
if hook.end is not None and hook.end in attr:
attr[hook.end].remove(hook)
else:
attr.remove(hook)
if module_src is not None:
module_src.REGISTERED_HOOKS.remove((store, hook))
class Hook:
"""Class storing hook informations"""
def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, end=None, call_end=None):
self.name = name
self.end = end
self.call = call
if call_end is None:
self.call_end = self.call
else:
self.call_end = call_end
self.regexp = regexp
self.data = data
self.times = -1
self.server = server
self.channels = channels
def is_matching(self, strcmp, channel=None, server=None):
"""Test if the current hook correspond to the message"""
return (channel is None or len(self.channels) <= 0 or
channel in self.channels) and (server is None or
self.server is None or self.server == server) and (
(self.name is None or strcmp == self.name) and (
self.end is None or strcmp == self.end) and (
self.regexp is None or re.match(self.regexp, strcmp)))
def run(self, msg, data2=None, strcmp=None):
"""Run the hook"""
if self.times != 0:
self.times -= 1
if (self.end is not None and strcmp is not None and
self.call_end is not None and strcmp == self.end):
call = self.call_end
self.times = 0
else:
call = self.call
if self.data is None:
if data2 is None:
return call(msg)
elif isinstance(data2, dict):
return call(msg, **data2)
else:
return call(msg, data2)
elif isinstance(self.data, dict):
if data2 is None:
return call(msg, **self.data)
else:
return call(msg, data2, **self.data)
else:
if data2 is None:
return call(msg, self.data)
elif isinstance(data2, dict):
return call(msg, self.data, **data2)
else:
return call(msg, self.data, data2)

View file

@ -1,264 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from importlib.abc import Finder
from importlib.abc import SourceLoader
import imp
import os
import sys
import event
from hooks import Hook
import response
import xmlparser
class ModuleFinder(Finder):
def __init__(self, context, prompt):
self.context = context
self.prompt = prompt
def find_module(self, fullname, path=None):
#print ("looking for", fullname, "in", path)
# Search only for new nemubot modules (packages init)
if path is None:
for mpath in self.context.modules_path:
#print ("looking for", fullname, "in", mpath)
if os.path.isfile(mpath + fullname + ".xml"):
return ModuleLoader(self.context, self.prompt, fullname,
mpath, mpath + fullname + ".xml")
elif (os.path.isfile(mpath + fullname + ".py") or
os.path.isfile(mpath + fullname + "/__init__.py")):
return ModuleLoader(self.context, self.prompt,
fullname, mpath, None)
#print ("not found")
return None
class ModuleLoader(SourceLoader):
def __init__(self, context, prompt, fullname, path, config_path):
self.context = context
self.prompt = prompt
self.name = fullname
self.config_path = config_path
if config_path is not None:
self.config = xmlparser.parse_file(config_path)
if self.config.hasAttribute("name"):
self.name = self.config["name"]
else:
self.config = None
if os.path.isfile(path + fullname + ".py"):
self.source_path = path + self.name + ".py"
self.package = False
self.mpath = path
elif os.path.isfile(path + fullname + "/__init__.py"):
self.source_path = path + self.name + "/__init__.py"
self.package = True
self.mpath = path + self.name + "/"
else:
raise ImportError
def get_filename(self, fullname):
"""Return the path to the source file as found by the finder."""
return self.source_path
def get_data(self, path):
"""Return the data from path as raw bytes."""
with open(path, 'rb') as file:
return file.read()
def path_mtime(self, path):
st = os.stat(path)
return int(st.st_mtime)
def set_data(self, path, data):
"""Write bytes data to a file."""
parent, filename = os.path.split(path)
path_parts = []
# Figure out what directories are missing.
while parent and not os.path.isdir(parent):
parent, part = os.path.split(parent)
path_parts.append(part)
# Create needed directories.
for part in reversed(path_parts):
parent = os.path.join(parent, part)
try:
os.mkdir(parent)
except FileExistsError:
# Probably another Python process already created the dir.
continue
except PermissionError:
# If can't get proper access, then just forget about writing
# the data.
return
try:
with open(path, 'wb') as file:
file.write(data)
except (PermissionError, FileExistsError):
pass
def get_code(self, fullname):
return SourceLoader.get_code(self, fullname)
def get_source(self, fullname):
return SourceLoader.get_source(self, fullname)
def is_package(self, fullname):
return self.package
def load_module(self, fullname):
module = self._load_module(fullname, sourceless=True)
# Remove the module from sys list
del sys.modules[fullname]
# If the module was already loaded, then reload it
if hasattr(module, '__LOADED__'):
reload(module)
# Check that is a valid nemubot module
if not hasattr(module, "nemubotversion"):
raise ImportError("Module `%s' is not a nemubot module."%self.name)
# Check module version
if module.nemubotversion != self.context.version:
raise ImportError("Module `%s' is not compatible with this "
"version." % self.name)
# Set module common functions and datas
module.__LOADED__ = True
# Set module common functions and datas
module.REGISTERED_HOOKS = list()
module.REGISTERED_EVENTS = list()
module.DEBUG = False
module.DIR = self.mpath
module.name = fullname
module.print = lambda msg: print("[%s] %s"%(module.name, msg))
module.print_debug = lambda msg: mod_print_dbg(module, msg)
module.send_response = lambda srv, res: mod_send_response(self.context, srv, res)
module.add_hook = lambda store, hook: self.context.hooks.add_hook(store, hook, module)
module.del_hook = lambda store, hook: self.context.hooks.del_hook(store, hook)
module.add_event = lambda evt: self.context.add_event(evt, module_src=module)
module.add_event_eid = lambda evt, eid: self.context.add_event(evt, eid, module_src=module)
module.del_event = lambda evt: self.context.del_event(evt, module_src=module)
if not hasattr(module, "NODATA"):
module.DATAS = xmlparser.parse_file(self.context.datas_path
+ module.name + ".xml")
module.save = lambda: mod_save(module, self.context.datas_path)
else:
module.DATAS = None
module.save = lambda: False
module.CONF = self.config
module.has_access = lambda msg: mod_has_access(module,
module.CONF, msg)
module.ModuleEvent = event.ModuleEvent
module.ModuleState = xmlparser.module_state.ModuleState
module.Response = response.Response
# Load dependancies
if module.CONF is not None and module.CONF.hasNode("dependson"):
module.MODS = dict()
for depend in module.CONF.getNodes("dependson"):
for md in MODS:
if md.name == depend["name"]:
mod.MODS[md.name] = md
break
if depend["name"] not in module.MODS:
print ("\033[1;31mERROR:\033[0m in module `%s', module "
"`%s' require by this module but is not loaded."
% (module.name, depend["name"]))
return
# Add the module to the global modules list
if self.context.add_module(module):
# Launch the module
if hasattr(module, "load"):
module.load(self.context)
# Register hooks
register_hooks(module, self.context, self.prompt)
print (" Module `%s' successfully loaded." % module.name)
else:
raise ImportError("An error occurs while importing `%s'."
% module.name)
return module
def add_cap_hook(prompt, module, cmd):
if hasattr(module, cmd["call"]):
prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"]))
else:
print ("Warning: In module `%s', no function `%s' defined for `%s' "
"command hook." % (module.name, cmd["call"], cmd["name"]))
def register_hooks(module, context, prompt):
"""Register all available hooks"""
if module.CONF is not None:
# Register command hooks
if module.CONF.hasNode("command"):
for cmd in module.CONF.getNodes("command"):
if cmd.hasAttribute("name") and cmd.hasAttribute("call"):
add_cap_hook(prompt, module, cmd)
# Register message hooks
if module.CONF.hasNode("message"):
for msg in module.CONF.getNodes("message"):
context.hooks.register_hook(module, msg)
# Register legacy hooks
if hasattr(module, "parseanswer"):
context.hooks.add_hook("cmd_default", Hook(module.parseanswer), module)
if hasattr(module, "parseask"):
context.hooks.add_hook("ask_default", Hook(module.parseask), module)
if hasattr(module, "parselisten"):
context.hooks.add_hook("msg_default", Hook(module.parselisten), module)
##########################
# #
# Module functions #
# #
##########################
def mod_print_dbg(mod, msg):
if mod.DEBUG:
print("{%s} %s"%(mod.name, msg))
def mod_save(mod, datas_path):
mod.DATAS.save(datas_path + "/" + mod.name + ".xml")
mod.print_debug("Saving!")
def mod_has_access(mod, config, msg):
if config is not None and config.hasNode("channel"):
for chan in config.getNodes("channel"):
if (chan["server"] is None or chan["server"] == msg.srv.id) and (
chan["channel"] is None or chan["channel"] == msg.channel):
return True
return False
else:
return True
def mod_send_response(context, server, res):
if server in context.servers:
context.servers[server].send_response(res, None)
else:
print("\033[1;35mWarning:\033[0m Try to send a message to the unknown server: %s" % server)

View file

@ -1,294 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
import re
import shlex
import time
import credits
from credits import Credits
from response import Response
import xmlparser
CREDITS = {}
filename = ""
def load(config_file):
global CREDITS, filename
CREDITS = dict ()
filename = config_file
credits.BANLIST = xmlparser.parse_file(filename)
def save():
global filename
credits.BANLIST.save(filename)
class Message:
def __init__ (self, line, timestamp, private = False):
self.raw = line
self.time = timestamp
self.channel = None
self.content = b''
self.ctcp = False
line = line.rstrip() #remove trailing 'rn'
words = line.split(b' ')
if words[0][0] == 58: #58 is : in ASCII table
self.sender = words[0][1:].decode()
self.cmd = words[1].decode()
else:
self.cmd = words[0].decode()
self.sender = None
if self.cmd == 'PING':
self.content = words[1]
elif self.sender is not None:
self.nick = (self.sender.split('!'))[0]
if self.nick != self.sender:
self.realname = (self.sender.split('!'))[1]
else:
self.realname = self.nick
self.sender = self.nick + "!" + self.realname
if len(words) > 2:
self.channel = self.pickWords(words[2:]).decode()
if self.cmd == 'PRIVMSG':
# Check for CTCP request
self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01)
self.content = self.pickWords(words[3:])
elif self.cmd == '353' and len(words) > 3:
for i in range(2, len(words)):
if words[i][0] == 58:
self.content = words[i:]
#Remove the first :
self.content[0] = self.content[0][1:]
self.channel = words[i-1].decode()
break
elif self.cmd == 'NICK':
self.content = self.pickWords(words[2:])
elif self.cmd == 'MODE':
self.content = words[3:]
elif self.cmd == '332':
self.channel = words[3]
self.content = self.pickWords(words[4:])
else:
#print (line)
self.content = self.pickWords(words[3:])
else:
print (line)
if self.cmd == 'PRIVMSG':
self.channel = words[2].decode()
self.content = b' '.join(words[3:])
self.decode()
if self.cmd == 'PRIVMSG':
self.parse_content()
self.private = private
def parse_content(self):
"""Parse or reparse the message content"""
# If CTCP, remove 0x01
if self.ctcp:
self.content = self.content[1:len(self.content)-1]
# Split content by words
try:
self.cmds = shlex.split(self.content)
except ValueError:
self.cmds = self.content.split(' ')
def pickWords(self, words):
"""Parse last argument of a line: can be a single word or a sentence starting with :"""
if len(words) > 0 and len(words[0]) > 0:
if words[0][0] == 58:
return b' '.join(words[0:])[1:]
else:
return words[0]
else:
return b''
def decode(self):
"""Decode the content string usign a specific encoding"""
if isinstance(self.content, bytes):
try:
self.content = self.content.decode()
except UnicodeDecodeError:
#TODO: use encoding from config file
self.content = self.content.decode('utf-8', 'replace')
def authorize_DEPRECATED(self):
"""Is nemubot listening for the sender on this channel?"""
# TODO: deprecated
if self.srv.isDCC(self.sender):
return True
elif self.realname not in CREDITS:
CREDITS[self.realname] = Credits(self.realname)
elif self.content[0] == '`':
return True
elif not CREDITS[self.realname].ask():
return False
return self.srv.accepted_channel(self.channel)
##############################
# #
# Extraction/Format text #
# #
##############################
def just_countdown (self, delta, resolution = 5):
sec = delta.seconds
hours, remainder = divmod(sec, 3600)
minutes, seconds = divmod(remainder, 60)
an = int(delta.days / 365.25)
days = delta.days % 365.25
sentence = ""
force = False
if resolution > 0 and (force or an > 0):
force = True
sentence += " %i an"%(an)
if an > 1:
sentence += "s"
if resolution > 2:
sentence += ","
elif resolution > 1:
sentence += " et"
if resolution > 1 and (force or days > 0):
force = True
sentence += " %i jour"%(days)
if days > 1:
sentence += "s"
if resolution > 3:
sentence += ","
elif resolution > 2:
sentence += " et"
if resolution > 2 and (force or hours > 0):
force = True
sentence += " %i heure"%(hours)
if hours > 1:
sentence += "s"
if resolution > 4:
sentence += ","
elif resolution > 3:
sentence += " et"
if resolution > 3 and (force or minutes > 0):
force = True
sentence += " %i minute"%(minutes)
if minutes > 1:
sentence += "s"
if resolution > 4:
sentence += " et"
if resolution > 4 and (force or seconds > 0):
force = True
sentence += " %i seconde"%(seconds)
if seconds > 1:
sentence += "s"
return sentence[1:]
def countdown_format (self, date, msg_before, msg_after, timezone = None):
"""Replace in a text %s by a sentence incidated the remaining time before/after an event"""
if timezone != None:
os.environ['TZ'] = timezone
time.tzset()
#Calculate time before the date
if datetime.now() > date:
sentence_c = msg_after
delta = datetime.now() - date
else:
sentence_c = msg_before
delta = date - datetime.now()
if timezone != None:
os.environ['TZ'] = "Europe/Paris"
return sentence_c % self.just_countdown(delta)
def extractDate (self):
"""Parse a message to extract a time and date"""
msgl = self.content.lower ()
result = re.match("^[^0-9]+(([0-9]{1,4})[^0-9]+([0-9]{1,2}|janvier|january|fevrier|février|february|mars|march|avril|april|mai|maï|may|juin|juni|juillet|july|jully|august|aout|août|septembre|september|october|octobre|oktober|novembre|november|decembre|décembre|december)([^0-9]+([0-9]{1,4}))?)[^0-9]+(([0-9]{1,2})[^0-9]*[h':]([^0-9]*([0-9]{1,2})([^0-9]*[m\":][^0-9]*([0-9]{1,2}))?)?)?.*$", msgl + " TXT")
if result is not None:
day = result.group(2)
if len(day) == 4:
year = day
day = 0
month = result.group(3)
if month == "janvier" or month == "january" or month == "januar":
month = 1
elif month == "fevrier" or month == "février" or month == "february":
month = 2
elif month == "mars" or month == "march":
month = 3
elif month == "avril" or month == "april":
month = 4
elif month == "mai" or month == "may" or month == "maï":
month = 5
elif month == "juin" or month == "juni" or month == "junni":
month = 6
elif month == "juillet" or month == "jully" or month == "july":
month = 7
elif month == "aout" or month == "août" or month == "august":
month = 8
elif month == "september" or month == "septembre":
month = 9
elif month == "october" or month == "october" or month == "oktober":
month = 10
elif month == "november" or month == "novembre":
month = 11
elif month == "december" or month == "decembre" or month == "décembre":
month = 12
if day == 0:
day = result.group(5)
else:
year = result.group(5)
hour = result.group(7)
minute = result.group(9)
second = result.group(11)
print ("Chaîne reconnue : %s/%s/%s %s:%s:%s"%(day, month, year, hour, minute, second))
if year == None:
year = date.today().year
if hour == None:
hour = 0
if minute == None:
minute = 0
if second == None:
second = 1
else:
second = int (second) + 1
if second > 59:
minute = int (minute) + 1
second = 0
return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second))
else:
return None

277
modules/alias.py Normal file
View file

@ -0,0 +1,277 @@
"""Create alias of commands"""
# PYTHON STUFFS #######################################################
import re
from datetime import datetime, timezone
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.message import Command
from nemubot.tools.human import guess
from nemubot.tools.xmlparser.node import ModuleState
from nemubot.module.more import Response
# LOADING #############################################################
def load(context):
"""Load this module"""
if not context.data.hasNode("aliases"):
context.data.addChild(ModuleState("aliases"))
context.data.getNode("aliases").setIndex("alias")
if not context.data.hasNode("variables"):
context.data.addChild(ModuleState("variables"))
context.data.getNode("variables").setIndex("name")
# MODULE CORE #########################################################
## Alias management
def list_alias(channel=None):
"""List known aliases.
Argument:
channel -- optional, if defined, return a list of aliases only defined on this channel, else alias widly defined
"""
for alias in context.data.getNode("aliases").index.values():
if (channel is None and "channel" not in alias) or (channel is not None and "channel" in alias and alias["channel"] == channel):
yield alias
def create_alias(alias, origin, channel=None, creator=None):
"""Create or erase an existing alias
"""
anode = ModuleState("alias")
anode["alias"] = alias
anode["origin"] = origin
if channel is not None:
anode["creator"] = channel
if creator is not None:
anode["creator"] = creator
context.data.getNode("aliases").addChild(anode)
context.save()
## Variables management
def get_variable(name, msg=None):
"""Get the value for the given variable
Arguments:
name -- The variable identifier
msg -- optional, original message where some variable can be picked
"""
if msg is not None and (name == "sender" or name == "from" or name == "nick"):
return msg.frm
elif msg is not None and (name == "chan" or name == "channel"):
return msg.channel
elif name == "date":
return datetime.now(timezone.utc).strftime("%c")
elif name in context.data.getNode("variables").index:
return context.data.getNode("variables").index[name]["value"]
else:
return None
def list_variables(user=None):
"""List known variables.
Argument:
user -- optional, if defined, display only variable created by the given user
"""
if user is not None:
return [x for x in context.data.getNode("variables").index.values() if x["creator"] == user]
else:
return context.data.getNode("variables").index.values()
def set_variable(name, value, creator):
"""Define or erase a variable.
Arguments:
name -- The variable identifier
value -- Variable value
creator -- User who has created this variable
"""
var = ModuleState("variable")
var["name"] = name
var["value"] = value
var["creator"] = creator
context.data.getNode("variables").addChild(var)
context.save()
def replace_variables(cnts, msg):
"""Replace variables contained in the content
Arguments:
cnt -- content where search variables
msg -- Message where pick some variables
"""
unsetCnt = list()
if not isinstance(cnts, list):
cnts = list(cnts)
resultCnt = list()
for cnt in cnts:
for res, name, default in re.findall("\\$\{(([a-zA-Z0-9:]+)(?:-([^}]+))?)\}", cnt):
rv = re.match("([0-9]+)(:([0-9]*))?", name)
if rv is not None:
varI = int(rv.group(1)) - 1
if varI >= len(msg.args):
cnt = cnt.replace("${%s}" % res, default, 1)
elif rv.group(2) is not None:
if rv.group(3) is not None and len(rv.group(3)):
varJ = int(rv.group(3)) - 1
cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:varJ]), 1)
for v in range(varI, varJ):
unsetCnt.append(v)
else:
cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:]), 1)
for v in range(varI, len(msg.args)):
unsetCnt.append(v)
else:
cnt = cnt.replace("${%s}" % res, msg.args[varI], 1)
unsetCnt.append(varI)
else:
cnt = cnt.replace("${%s}" % res, get_variable(name) or default, 1)
resultCnt.append(cnt)
# Remove used content
for u in sorted(set(unsetCnt), reverse=True):
msg.args.pop(u)
return resultCnt
# MODULE INTERFACE ####################################################
## Variables management
@hook.command("listvars",
help="list defined variables for substitution in input commands",
help_usage={
None: "List all known variables",
"USER": "List variables created by USER"})
def cmd_listvars(msg):
if len(msg.args):
res = list()
for user in msg.args:
als = [v["name"] for v in list_variables(user)]
if len(als) > 0:
res.append("%s's variables: %s" % (user, ", ".join(als)))
else:
res.append("%s didn't create variable yet." % user)
return Response(" ; ".join(res), channel=msg.channel)
elif len(context.data.getNode("variables").index):
return Response(list_variables(),
channel=msg.channel,
title="Known variables")
else:
return Response("There is currently no variable stored.", channel=msg.channel)
@hook.command("set",
help="Create or set variables for substitution in input commands",
help_usage={"KEY VALUE": "Define the variable named KEY and fill it with VALUE as content"})
def cmd_set(msg):
if len(msg.args) < 2:
raise IMException("!set take two args: the key and the value.")
set_variable(msg.args[0], " ".join(msg.args[1:]), msg.frm)
return Response("Variable $%s successfully defined." % msg.args[0],
channel=msg.channel)
## Alias management
@hook.command("listalias",
help="List registered aliases",
help_usage={
None: "List all registered aliases",
"USER": "List all aliases created by USER"})
def cmd_listalias(msg):
aliases = [a for a in list_alias(None)] + [a for a in list_alias(msg.channel)]
if len(aliases):
return Response([a["alias"] for a in aliases],
channel=msg.channel,
title="Known aliases")
return Response("There is no alias currently.", channel=msg.channel)
@hook.command("alias",
help="Display or define the replacement command for a given alias",
help_usage={
"ALIAS": "Extends the given alias",
"ALIAS COMMAND [ARGS ...]": "Create a new alias named ALIAS as replacement to the given COMMAND and ARGS",
})
def cmd_alias(msg):
if not len(msg.args):
raise IMException("!alias takes as argument an alias to extend.")
alias = context.subparse(msg, msg.args[0])
if alias is None or not isinstance(alias, Command):
raise IMException("%s is not a valid alias" % msg.args[0])
if alias.cmd in context.data.getNode("aliases").index:
return Response("%s corresponds to %s" % (alias.cmd, context.data.getNode("aliases").index[alias.cmd]["origin"]),
channel=msg.channel, nick=msg.frm)
elif len(msg.args) > 1:
create_alias(alias.cmd,
" ".join(msg.args[1:]),
channel=msg.channel,
creator=msg.frm)
return Response("New alias %s successfully registered." % alias.cmd,
channel=msg.channel)
else:
wym = [m for m in guess(alias.cmd, context.data.getNode("aliases").index)]
raise IMException(msg.args[0] + " is not an alias." + (" Would you mean: %s?" % ", ".join(wym) if len(wym) else ""))
@hook.command("unalias",
help="Remove a previously created alias")
def cmd_unalias(msg):
if not len(msg.args):
raise IMException("Which alias would you want to remove?")
res = list()
for alias in msg.args:
if alias[0] == "!" and len(alias) > 1:
alias = alias[1:]
if alias in context.data.getNode("aliases").index:
context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias])
res.append(Response("%s doesn't exist anymore." % alias,
channel=msg.channel))
else:
res.append(Response("%s is not an alias" % alias,
channel=msg.channel))
return res
## Alias replacement
@hook.add(["pre","Command"])
def treat_alias(msg):
if context.data.getNode("aliases") is not None and msg.cmd in context.data.getNode("aliases").index:
origin = context.data.getNode("aliases").index[msg.cmd]["origin"]
rpl_msg = context.subparse(msg, origin)
if isinstance(rpl_msg, Command):
rpl_msg.args = replace_variables(rpl_msg.args, msg)
rpl_msg.args += msg.args
rpl_msg.kwargs.update(msg.kwargs)
elif len(msg.args) or len(msg.kwargs):
raise IMException("This kind of alias doesn't take any argument (haven't you forgotten the '!'?).")
# Avoid infinite recursion
if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd:
return rpl_msg
return msg

View file

@ -1,156 +0,0 @@
# coding=utf-8
import re
import sys
from datetime import datetime
nemubotversion = 3.3
def load(context):
"""Load this module"""
from hooks import Hook
add_hook("cmd_hook", Hook(cmd_unalias, "unalias"))
add_hook("cmd_hook", Hook(cmd_alias, "alias"))
add_hook("cmd_hook", Hook(cmd_set, "set"))
add_hook("all_pre", Hook(treat_alias))
add_hook("all_post", Hook(treat_variables))
global DATAS
if not DATAS.hasNode("aliases"):
DATAS.addChild(ModuleState("aliases"))
DATAS.getNode("aliases").setIndex("alias")
if not DATAS.hasNode("variables"):
DATAS.addChild(ModuleState("variables"))
DATAS.getNode("variables").setIndex("name")
def help_tiny ():
"""Line inserted in the response to the command !help"""
return "alias module"
def help_full ():
return "TODO"
def set_variable(name, value):
var = ModuleState("variable")
var["name"] = name
var["value"] = value
DATAS.getNode("variables").addChild(var)
def get_variable(name, msg=None):
if name == "sender":
return msg.sender
elif name == "nick":
return msg.nick
elif name == "chan" or name == "channel":
return msg.channel
elif name == "date":
now = datetime.now()
return ("%d/%d/%d %d:%d:%d"%(now.day, now.month, now.year, now.hour,
now.minute, now.second))
elif name in DATAS.getNode("variables").index:
return DATAS.getNode("variables").index[name]["value"]
else:
return ""
def cmd_set(msg):
if len (msg.cmds) > 2:
set_variable(msg.cmds[1], " ".join(msg.cmds[2:]))
res = Response(msg.sender, "Variable \$%s définie." % msg.cmds[1])
save()
return res
return Response(msg.sender, "!set prend au minimum deux arguments : le nom de la variable et sa valeur.")
def cmd_alias(msg):
if len (msg.cmds) > 1:
res = list()
for alias in msg.cmds[1:]:
if alias[0] == "!":
alias = alias[1:]
if alias in DATAS.getNode("aliases").index:
res.append(Response(msg.sender, "!%s correspond à %s" % (alias,
DATAS.getNode("aliases").index[alias]["origin"]),
channel=msg.channel))
else:
res.append(Response(msg.sender, "!%s n'est pas un alias" % alias,
channel=msg.channel))
return res
else:
return Response(msg.sender, "!alias prend en argument l'alias à étendre.",
channel=msg.channel)
def cmd_unalias(msg):
if len (msg.cmds) > 1:
res = list()
for alias in msg.cmds[1:]:
if alias[0] == "!" and len(alias) > 1:
alias = alias[1:]
if alias in DATAS.getNode("aliases").index:
if DATAS.getNode("aliases").index[alias]["creator"] == msg.nick or msg.is_owner:
DATAS.getNode("aliases").delChild(DATAS.getNode("aliases").index[alias])
res.append(Response(msg.sender, "%s a bien été supprimé" % alias, channel=msg.channel))
else:
res.append(Response(msg.sender, "Vous n'êtes pas le createur de l'alias %s." % alias, channel=msg.channel))
else:
res.append(Response(msg.sender, "%s n'est pas un alias" % alias, channel=msg.channel))
return res
else:
return Response(msg.sender, "!unalias prend en argument l'alias à supprimer.", channel=msg.channel)
def replace_variables(cnt, msg=None):
cnt = cnt.split(' ')
unsetCnt = list()
for i in range(0, len(cnt)):
if i not in unsetCnt:
res = re.match("^([^$]*)(\\\\)?\\$([a-zA-Z0-9]+)(.*)$", cnt[i])
if res is not None:
try:
varI = int(res.group(3))
unsetCnt.append(varI)
cnt[i] = res.group(1) + msg.cmds[varI] + res.group(4)
except:
if res.group(2) != "":
cnt[i] = res.group(1) + "$" + res.group(3) + res.group(4)
else:
cnt[i] = res.group(1) + get_variable(res.group(3), msg) + res.group(4)
return " ".join(cnt)
def treat_variables(res):
for i in range(0, len(res.messages)):
if isinstance(res.messages[i], list):
res.messages[i] = replace_variables(", ".join(res.messages[i]), res)
else:
res.messages[i] = replace_variables(res.messages[i], res)
return True
def treat_alias(msg, hooks_cache):
if msg.cmd == "PRIVMSG" and (len(msg.cmds[0]) > 0 and msg.cmds[0][0] == "!"
and msg.cmds[0][1:] in DATAS.getNode("aliases").index
and msg.cmds[0][1:] not in hooks_cache("cmd_hook")):
msg.content = msg.content.replace(msg.cmds[0],
DATAS.getNode("aliases").index[msg.cmds[0][1:]]["origin"], 1)
msg.content = replace_variables(msg.content, msg)
msg.parse_content()
return True
return False
def parseask(msg):
global ALIAS
if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.content) is not None:
result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.content)
if result.group(1) in DATAS.getNode("aliases").index or result.group(3).find("alias") >= 0:
return Response(msg.sender, "Cet alias est déjà défini.")
else:
alias = ModuleState("alias")
alias["alias"] = result.group(1)
alias["origin"] = result.group(3)
alias["creator"] = msg.nick
DATAS.getNode("aliases").addChild(alias)
res = Response(msg.sender, "Nouvel alias %s défini avec succès." % result.group(1))
save()
return res
return False

View file

@ -1,111 +1,134 @@
# coding=utf-8
"""People birthdays and ages"""
# PYTHON STUFFS #######################################################
import re
import sys
from datetime import datetime
from datetime import date
from datetime import date, datetime
from xmlparser.node import ModuleState
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools.countdown import countdown_format
from nemubot.tools.date import extractDate
from nemubot.tools.xmlparser.node import ModuleState
nemubotversion = 3.3
from nemubot.module.more import Response
# LOADING #############################################################
def load(context):
global DATAS
DATAS.setIndex("name", "birthday")
context.data.setIndex("name", "birthday")
def help_tiny ():
"""Line inserted in the response to the command !help"""
return "People birthdays and ages"
def help_full ():
return "!anniv /who/: gives the remaining time before the anniversary of /who/\n!age /who/: gives the age of /who/\nIf /who/ is not given, gives the remaining time before your anniversary.\n\n To set yout birthday, say it to nemubot :)"
# MODULE CORE #########################################################
def findName(msg):
if len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or msg.cmds[1].lower() == "me":
name = msg.nick.lower()
if (not len(msg.args) or msg.args[0].lower() == "moi" or
msg.args[0].lower() == "me"):
name = msg.frm.lower()
else:
name = msg.cmds[1].lower()
name = msg.args[0].lower()
matches = []
if name in DATAS.index:
if name in context.data.index:
matches.append(name)
else:
for k in DATAS.index.keys ():
if k.find (name) == 0:
matches.append (k)
for k in context.data.index.keys():
if k.find(name) == 0:
matches.append(k)
return (matches, name)
# MODULE INTERFACE ####################################################
## Commands
@hook.command("anniv",
help="gives the remaining time before the anniversary of known people",
help_usage={
None: "Calculate the time remaining before your birthday",
"WHO": "Calculate the time remaining before WHO's birthday",
})
def cmd_anniv(msg):
(matches, name) = findName(msg)
if len(matches) == 1:
name = matches[0]
tyd = DATAS.index[name].getDate("born")
tyd = context.data.index[name].getDate("born")
tyd = datetime(date.today().year, tyd.month, tyd.day)
if (tyd.day == datetime.today().day and
tyd.month == datetime.today().month):
return Response(msg.sender, msg.countdown_format(
DATAS.index[name].getDate("born"), "",
"C'est aujourd'hui l'anniversaire de %s !"
" Il a %s. Joyeux anniversaire :)" % (name, "%s")),
return Response(countdown_format(
context.data.index[name].getDate("born"), "",
"C'est aujourd'hui l'anniversaire de %s !"
" Il a %s. Joyeux anniversaire :)" % (name, "%s")),
msg.channel)
else:
if tyd < datetime.today():
tyd = datetime(date.today().year + 1, tyd.month, tyd.day)
return Response(msg.sender, msg.countdown_format(tyd,
return Response(countdown_format(tyd,
"Il reste %s avant l'anniversaire de %s !" % ("%s",
name), ""),
msg.channel)
else:
return Response(msg.sender, "désolé, je ne connais pas la date d'anniversaire"
return Response("désolé, je ne connais pas la date d'anniversaire"
" de %s. Quand est-il né ?" % name,
msg.channel, msg.nick)
msg.channel, msg.frm)
@hook.command("age",
help="Calculate age of known people",
help_usage={
None: "Calculate your age",
"WHO": "Calculate the age of WHO"
})
def cmd_age(msg):
(matches, name) = findName(msg)
if len(matches) == 1:
name = matches[0]
d = DATAS.index[name].getDate("born")
d = context.data.index[name].getDate("born")
return Response(msg.sender, msg.countdown_format(d,
"%s va naître dans %s." % (name, "%s"),
"%s a %s." % (name, "%s")),
return Response(countdown_format(d,
"%s va naître dans %s." % (name, "%s"),
"%s a %s." % (name, "%s")),
msg.channel)
else:
return Response(msg.sender, "désolé, je ne connais pas l'âge de %s."
" Quand est-il né ?" % name, msg.channel, msg.nick)
return Response("désolé, je ne connais pas l'âge de %s."
" Quand est-il né ?" % name, msg.channel, msg.frm)
return True
## Input parsing
@hook.ask()
def parseask(msg):
msgl = msg.content.lower ()
if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl) is not None:
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.message, re.I)
if res is not None:
try:
extDate = msg.extractDate()
extDate = extractDate(msg.message)
if extDate is None or extDate.year > datetime.now().year:
return Response(msg.sender,
"ta date de naissance ne paraît pas valide...",
return Response("la date de naissance ne paraît pas valide...",
msg.channel,
msg.nick)
msg.frm)
else:
if msg.nick.lower() in DATAS.index:
DATAS.index[msg.nick.lower()]["born"] = extDate
nick = res.group(1)
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
nick = msg.frm
if nick.lower() in context.data.index:
context.data.index[nick.lower()]["born"] = extDate
else:
ms = ModuleState("birthday")
ms.setAttribute("name", msg.nick.lower())
ms.setAttribute("name", nick.lower())
ms.setAttribute("born", extDate)
DATAS.addChild(ms)
save()
return Response(msg.sender,
"ok, c'est noté, ta date de naissance est le %s"
% extDate.strftime("%A %d %B %Y à %H:%M"),
context.data.addChild(ms)
context.save()
return Response("ok, c'est noté, %s est né le %s"
% (nick, extDate.strftime("%A %d %B %Y à %H:%M")),
msg.channel,
msg.nick)
msg.frm)
except:
return Response(msg.sender, "ta date de naissance ne paraît pas valide...",
msg.channel, msg.nick)
raise IMException("la date de naissance ne paraît pas valide.")

View file

@ -1,5 +0,0 @@
<?xml version="1.0" ?>
<nemubotmodule name="birthday">
<message type="cmd" name="anniv" call="cmd_anniv" />
<message type="cmd" name="age" call="cmd_age" />
</nemubotmodule>

View file

@ -1,51 +1,74 @@
# coding=utf-8
"""Wishes Happy New Year when the time comes"""
from datetime import datetime
# PYTHON STUFFS #######################################################
nemubotversion = 3.3
from datetime import datetime, timezone
from nemubot.event import ModuleEvent
from nemubot.hooks import hook
from nemubot.tools.countdown import countdown_format
from nemubot.module.more import Response
# GLOBALS #############################################################
yr = datetime.now(timezone.utc).year
yrn = datetime.now(timezone.utc).year + 1
# LOADING #############################################################
def load(context):
yr = datetime.today().year
yrn = datetime.today().year + 1
if not context.config or not context.config.hasNode("sayon"):
print("You can append in your configuration some balise to "
"automaticaly wish an happy new year on some channels like:\n"
"<sayon hostid=\"nemubot@irc.freenode.net:6667\" "
"channel=\"#nemutest\" />")
d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now()
# d = datetime(yr, 12, 31, 19, 34, 0) - datetime.now()
add_event(ModuleEvent(intervalle=0, offset=d.total_seconds(), call=bonneannee))
def bonneannee():
txt = "Bonne année %d !" % yrn
print(txt)
if context.config and context.config.hasNode("sayon"):
for sayon in context.config.getNodes("sayon"):
if "hostid" not in sayon or "channel" not in sayon:
print("Error: missing hostif or channel")
continue
srv = sayon["hostid"]
chan = sayon["channel"]
context.send_response(srv, Response(txt, chan))
from hooks import Hook
add_hook("cmd_rgxp", Hook(cmd_timetoyear, data=yrn, regexp="^[0-9]{4}$"))
add_hook("cmd_hook", Hook(cmd_newyear, str(yrn), yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "ny", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "newyear", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "new-year", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "new year", yrn))
d = datetime(yrn, 1, 1, 0, 0, 0, 0,
timezone.utc) - datetime.now(timezone.utc)
context.add_event(ModuleEvent(interval=0, offset=d.total_seconds(),
call=bonneannee))
def bonneannee():
txt = "Bonne année %d !" % datetime.today().year
print (txt)
send_response("localhost:2771", Response(None, txt, "#epitagueule"))
send_response("localhost:2771", Response(None, txt, "#yaka"))
send_response("localhost:2771", Response(None, txt, "#epita2014"))
send_response("localhost:2771", Response(None, txt, "#ykar"))
send_response("localhost:2771", Response(None, txt, "#ordissimo"))
send_response("localhost:2771", Response(None, txt, "#42sh"))
send_response("localhost:2771", Response(None, txt, "#nemubot"))
def cmd_newyear(msg, yr):
return Response(msg.sender,
msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1),
"Il reste %s avant la nouvelle année.",
"Nous faisons déjà la fête depuis %s !"),
# MODULE INTERFACE ####################################################
@hook.command("newyear",
help="Display the remaining time before the next new year")
@hook.command(str(yrn),
help="Display the remaining time before %d" % yrn)
def cmd_newyear(msg):
return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0,
timezone.utc),
"Il reste %s avant la nouvelle année.",
"Nous faisons déjà la fête depuis %s !"),
channel=msg.channel)
@hook.command(data=yrn, regexp="^[0-9]{4}$",
help="Calculate time remaining/passed before/since the requested year")
def cmd_timetoyear(msg, cur):
yr = int(msg.cmds[0])
yr = int(msg.cmd)
if yr == cur:
return None
return Response(msg.sender,
msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1),
"Il reste %s avant %d." % ("%s", yr),
"Le premier janvier %d est passé depuis %s !" % (yr, "%s")),
return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0,
timezone.utc),
"Il reste %s avant %d." % ("%s", yr),
"Le premier janvier %d est passé "
"depuis %s !" % (yr, "%s")),
channel=msg.channel)

115
modules/books.py Normal file
View file

@ -0,0 +1,115 @@
"""Looking for books"""
# PYTHON STUFFS #######################################################
import urllib
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
# LOADING #############################################################
def load(context):
if not context.config or "goodreadskey" not in context.config:
raise ImportError("You need a Goodreads API key in order to use this "
"module. Add it to the module configuration file:\n"
"<module name=\"books\" goodreadskey=\"XXXXXX\" />\n"
"Get one at https://www.goodreads.com/api/keys")
# MODULE CORE #########################################################
def get_book(title):
"""Retrieve a book from its title"""
response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" %
(context.config["goodreadskey"], urllib.parse.quote(title)))
if response is not None and len(response.getElementsByTagName("book")):
return response.getElementsByTagName("book")[0]
else:
return None
def search_books(title):
"""Get a list of book matching given title"""
response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" %
(context.config["goodreadskey"], urllib.parse.quote(title)))
if response is not None and len(response.getElementsByTagName("search")):
return response.getElementsByTagName("search")[0].getElementsByTagName("results")[0].getElementsByTagName("work")
else:
return []
def search_author(name):
"""Looking for an author"""
response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" %
(urllib.parse.quote(name), context.config["goodreadskey"]))
if response is not None and len(response.getElementsByTagName("author")) and response.getElementsByTagName("author")[0].hasAttribute("id"):
response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" %
(urllib.parse.quote(response.getElementsByTagName("author")[0].getAttribute("id")), context.config["goodreadskey"]))
if response is not None and len(response.getElementsByTagName("author")):
return response.getElementsByTagName("author")[0]
return None
# MODULE INTERFACE ####################################################
@hook.command("book",
help="Get information about a book from its title",
help_usage={
"TITLE": "Get information about a book titled TITLE"
})
def cmd_book(msg):
if not len(msg.args):
raise IMException("please give me a title to search")
book = get_book(" ".join(msg.args))
if book is None:
raise IMException("unable to find book named like this")
res = Response(channel=msg.channel)
res.append_message("%s, written by %s: %s" % (book.getElementsByTagName("title")[0].firstChild.nodeValue,
book.getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue,
web.striphtml(book.getElementsByTagName("description")[0].firstChild.nodeValue if book.getElementsByTagName("description")[0].firstChild else "")))
return res
@hook.command("search_books",
help="Search book's title",
help_usage={
"APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE"
})
def cmd_books(msg):
if not len(msg.args):
raise IMException("please give me a title to search")
title = " ".join(msg.args)
res = Response(channel=msg.channel,
title="%s" % (title),
count=" (%d more books)")
for book in search_books(title):
res.append_message("%s, writed by %s" % (book.getElementsByTagName("best_book")[0].getElementsByTagName("title")[0].firstChild.nodeValue,
book.getElementsByTagName("best_book")[0].getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue))
return res
@hook.command("author_books",
help="Looking for books writen by a given author",
help_usage={
"AUTHOR": "Looking for books writen by AUTHOR"
})
def cmd_author(msg):
if not len(msg.args):
raise IMException("please give me an author to search")
name = " ".join(msg.args)
ath = search_author(name)
if ath is None:
raise IMException("%s does not appear to be a published author." % name)
return Response([b.getElementsByTagName("title")[0].firstChild.nodeValue for b in ath.getElementsByTagName("book")],
channel=msg.channel,
title=ath.getElementsByTagName("name")[0].firstChild.nodeValue)

55
modules/cat.py Normal file
View file

@ -0,0 +1,55 @@
"""Concatenate commands"""
# PYTHON STUFFS #######################################################
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.message import Command, DirectAsk, Text
from nemubot.module.more import Response
# MODULE CORE #########################################################
def cat(msg, *terms):
res = Response(channel=msg.to_response, server=msg.server)
for term in terms:
m = context.subparse(msg, term)
if isinstance(m, Command) or isinstance(m, DirectAsk):
for r in context.subtreat(m):
if isinstance(r, Response):
for t in range(len(r.messages)):
res.append_message(r.messages[t],
title=r.rawtitle if not isinstance(r.rawtitle, list) else r.rawtitle[t])
elif isinstance(r, Text):
res.append_message(r.message)
elif isinstance(r, str):
res.append_message(r)
else:
res.append_message(term)
return res
# MODULE INTERFACE ####################################################
@hook.command("cat",
help="Concatenate responses of commands given as argument",
help_usage={"!SUBCMD [!SUBCMD [...]]": "Concatenate response of subcommands"},
keywords={
"merge": "Merge messages into the same",
})
def cmd_cat(msg):
if len(msg.args) < 1:
raise IMException("No subcommand to concatenate")
r = cat(msg, *msg.args)
if "merge" in msg.kwargs and len(r.messages) > 1:
r.messages = [ r.messages ]
return r

View file

@ -1,96 +0,0 @@
# coding=utf-8
from datetime import datetime
from datetime import timedelta
from urllib.parse import quote
from tools import web
nemubotversion = 3.3
def help_tiny ():
"""Line inserted in the response to the command !help"""
return "Gets informations about current and next Épita courses"
def help_full ():
return "!chronos [spé] : gives current and next courses."
def get_courses(classe=None, room=None, teacher=None, date=None):
url = CONF.getNode("server")["url"]
if classe is not None:
url += "&class=" + quote(classe)
if room is not None:
url += "&room=" + quote(room)
if teacher is not None:
url += "&teacher=" + quote(teacher)
#TODO: date, not implemented at 23.tf
print_debug(url)
response = web.getXML(url)
if response is not None:
print_debug(response)
return response.getNodes("course")
else:
return None
def get_next_courses(classe=None, room=None, teacher=None, date=None):
courses = get_courses(classe, room, teacher, date)
now = datetime.now()
for c in courses:
start = c.getFirstNode("start").getDate()
if now > start:
return c
return None
def get_near_courses(classe=None, room=None, teacher=None, date=None):
courses = get_courses(classe, room, teacher, date)
return courses[0]
def cmd_chronos(msg):
if len(msg.cmds) > 1:
classe = msg.cmds[1]
else:
classe = ""
res = Response(msg.sender, channel=msg.channel, nomore="Je n'ai pas d'autre cours à afficher")
courses = get_courses(classe)
print_debug(courses)
if courses is not None:
now = datetime.now()
tomorrow = now + timedelta(days=1)
for c in courses:
idc = c.getFirstNode("id").getContent()
crs = c.getFirstNode("title").getContent()
start = c.getFirstNode("start").getDate()
end = c.getFirstNode("end").getDate()
where = c.getFirstNode("where").getContent()
teacher = c.getFirstNode("teacher").getContent()
students = c.getFirstNode("students").getContent()
if now > start:
title = "Actuellement "
msg = "\x03\x02" + crs + "\x03\x02 jusqu'"
if end < tomorrow:
msg += "à \x03\x02" + end.strftime("%H:%M")
else:
msg += "au \x03\x02" + end.strftime("%a %d à %H:%M")
msg += "\x03\x02 en \x03\x02" + where + "\x03\x02"
else:
title = "Prochainement "
duration = (end - start).total_seconds() / 60
msg = "\x03\x02" + crs + "\x03\x02 le \x03\x02" + start.strftime("%a %d à %H:%M") + "\x03\x02 pour " + "%dh%02d" % (int(duration / 60), duration % 60) + " en \x03\x02" + where + "\x03\x02"
if teacher != "":
msg += " avec " + teacher
if students != "":
msg += " pour les " + students
res.append_message(msg, title)
else:
res.append_message("Aucun cours n'a été trouvé")
return res

View file

@ -1,6 +0,0 @@
<?xml version="1.0" ?>
<nemubotmodule name="chronos">
<server url="http://chronos.23.tf/index.php?xml" />
<message type="cmd" name="chronos" call="cmd_chronos" />
<message type="cmd" name="Χρονος" call="cmd_chronos" />
</nemubotmodule>

View file

@ -1,201 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from networkbot import NetworkBot
nemubotversion = 3.3
NODATA = True
def getserver(toks, context, prompt):
"""Choose the server in toks or prompt"""
if len(toks) > 1 and toks[0] in context.servers:
return (context.servers[toks[0]], toks[1:])
elif prompt.selectedServer is not None:
return (prompt.selectedServer, toks)
else:
return (None, toks)
def close(data, toks, context, prompt):
"""Disconnect and forget (remove from the servers list) the server"""
if len(toks) > 1:
for s in toks[1:]:
if s in servers:
context.servers[s].disconnect()
del context.servers[s]
else:
print ("close: server `%s' not found." % s)
elif prompt.selectedServer is not None:
prompt.selectedServer.disconnect()
del prompt.servers[selectedServer.id]
prompt.selectedServer = None
return
def connect(data, toks, context, prompt):
"""Make the connexion to a server"""
if len(toks) > 1:
for s in toks[1:]:
if s in context.servers:
context.servers[s].launch(context.receive_message)
else:
print ("connect: server `%s' not found." % s)
elif prompt.selectedServer is not None:
prompt.selectedServer.launch(context.receive_message)
else:
print (" Please SELECT a server or give its name in argument.")
def disconnect(data, toks, context, prompt):
"""Close the connection to a server"""
if len(toks) > 1:
for s in toks[1:]:
if s in context.servers:
if not context.servers[s].disconnect():
print ("disconnect: server `%s' already disconnected." % s)
else:
print ("disconnect: server `%s' not found." % s)
elif prompt.selectedServer is not None:
if not prompt.selectedServer.disconnect():
print ("disconnect: server `%s' already disconnected."
% prompt.selectedServer.id)
else:
print (" Please SELECT a server or give its name in argument.")
def discover(data, toks, context, prompt):
"""Discover a new bot on a server"""
(srv, toks) = getserver(toks, context, prompt)
if srv is not None:
for name in toks[1:]:
if "!" in name:
bot = context.add_networkbot(srv, name)
bot.connect()
else:
print (" %s is not a valid fullname, for example: nemubot!nemubotV3@bot.nemunai.re")
else:
print (" Please SELECT a server or give its name in first argument.")
def hotswap(data, toks, context, prompt):
"""Reload a server class"""
if len(toks) > 1:
print ("hotswap: apply only on selected server")
elif prompt.selectedServer is not None:
del context.servers[prompt.selectedServer.id]
srv = server.Server(selectedServer.node, selectedServer.nick,
selectedServer.owner, selectedServer.realname,
selectedServer.s)
context.servers[srv.id] = srv
prompt.selectedServer.kill()
prompt.selectedServer = srv
prompt.selectedServer.start()
else:
print (" Please SELECT a server or give its name in argument.")
def join(data, toks, context, prompt):
"""Join or leave a channel"""
rd = 1
if len(toks) <= rd:
print ("%s: not enough arguments." % toks[0])
return
if toks[rd] in context.servers:
srv = context.servers[toks[rd]]
rd += 1
elif prompt.selectedServer is not None:
srv = prompt.selectedServer
else:
print (" Please SELECT a server or give its name in argument.")
return
if len(toks) <= rd:
print ("%s: not enough arguments." % toks[0])
return
if toks[0] == "join":
if len(toks) > rd + 1:
srv.join(toks[rd], toks[rd + 1])
else:
srv.join(toks[rd])
elif toks[0] == "leave" or toks[0] == "part":
srv.leave(toks[rd])
return
def save_mod(data, toks, context, prompt):
"""Force save module data"""
if len(toks) < 2:
print ("save: not enough arguments.")
return
for mod in toks[1:]:
if mod in context.modules:
context.modules[mod].save()
print ("save: module `%s´ saved successfully" % mod)
else:
print ("save: no module named `%s´" % mod)
return
def send(data, toks, context, prompt):
"""Send a message on a channel"""
rd = 1
if len(toks) <= rd:
print ("send: not enough arguments.")
return
if toks[rd] in context.servers:
srv = context.servers[toks[rd]]
rd += 1
elif prompt.selectedServer is not None:
srv = prompt.selectedServer
else:
print (" Please SELECT a server or give its name in argument.")
return
if len(toks) <= rd:
print ("send: not enough arguments.")
return
#Check the server is connected
if not srv.connected:
print ("send: server `%s' not connected." % srv.id)
return
if toks[rd] in srv.channels:
chan = toks[rd]
rd += 1
else:
print ("send: channel `%s' not authorized in server `%s'."
% (toks[rd], srv.id))
return
if len(toks) <= rd:
print ("send: not enough arguments.")
return
srv.send_msg_final(chan, toks[rd])
return "done"
def zap(data, toks, context, prompt):
"""Hard change connexion state"""
if len(toks) > 1:
for s in toks[1:]:
if s in context.servers:
context.servers[s].connected = not context.servers[s].connected
else:
print ("zap: server `%s' not found." % s)
elif prompt.selectedServer is not None:
prompt.selectedServer.connected = not prompt.selectedServer.connected
else:
print (" Please SELECT a server or give its name in argument.")

View file

@ -1,14 +0,0 @@
<?xml version="1.0" ?>
<nemubotmodule name="cmd_server">
<command name="close" call="close" />
<command name="connect" call="connect" />
<command name="discover" call="discover" />
<command name="disconnect" call="disconnect" />
<command name="hotswap" call="hotswap" />
<command name="join" call="join" />
<command name="leave" call="join" />
<command name="part" call="join" />
<command name="save" call="save_mod" />
<command name="send" call="send" />
<command name="zap" call="zap" />
</nemubotmodule>

94
modules/conjugaison.py Normal file
View file

@ -0,0 +1,94 @@
"""Find french conjugaison"""
# PYTHON STUFFS #######################################################
from collections import defaultdict
import re
from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.tools.web import striphtml
from nemubot.module.more import Response
# GLOBALS #############################################################
s = [('present', '0'), ('présent', '0'), ('pr', '0'),
('passé simple', '12'), ('passe simple', '12'), ('ps', '12'),
('passé antérieur', '112'), ('passe anterieur', '112'), ('pa', '112'),
('passé composé', '100'), ('passe compose', '100'), ('pc', '100'),
('futur', '18'), ('f', '18'),
('futur antérieur', '118'), ('futur anterieur', '118'), ('fa', '118'),
('subjonctif présent', '24'), ('subjonctif present', '24'), ('spr', '24'),
('subjonctif passé', '124'), ('subjonctif passe', '124'), ('spa', '124'),
('plus que parfait', '106'), ('pqp', '106'),
('imparfait', '6'), ('ii', '6')]
d = defaultdict(list)
for k, v in s:
d[k].append(v)
# MODULE CORE #########################################################
def get_conjug(verb, stringTens):
url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
quote(verb.encode("ISO-8859-1")))
page = web.getURLContent(url)
if page is not None:
for line in page.split("\n"):
if re.search('<div class="modeBloc">', line) is not None:
return compute_line(line, stringTens)
return list()
def compute_line(line, stringTens):
try:
idTemps = d[stringTens]
except:
raise IMException("le temps demandé n'existe pas")
if len(idTemps) == 0:
raise IMException("le temps demandé n'existe pas")
index = line.index('<div id="temps' + idTemps[0] + '\"')
endIndex = line[index:].index('<div class=\"conjugBloc\"')
endIndex += index
newLine = line[index:endIndex]
res = list()
for elt in re.finditer("[p|/]>([^/]*/b>)", newLine):
res.append(striphtml(elt.group(1)
.replace("<b>", "\x02")
.replace("</b>", "\x0F")))
return res
# MODULE INTERFACE ####################################################
@hook.command("conjugaison",
help_usage={
"TENS VERB": "give the conjugaison for VERB in TENS."
})
def cmd_conjug(msg):
if len(msg.args) < 2:
raise IMException("donne moi un temps et un verbe, et je te donnerai "
"sa conjugaison!")
tens = ' '.join(msg.args[:-1])
verb = msg.args[-1]
conjug = get_conjug(verb, tens)
if len(conjug) > 0:
return Response(conjug, channel=msg.channel,
title="Conjugaison de %s" % verb)
else:
raise IMException("aucune conjugaison de '%s' n'a été trouvé" % verb)

View file

@ -1,64 +0,0 @@
# coding=utf-8
from tools import web
nemubotversion = 3.3
def help_tiny ():
"""Line inserted in the response to the command !help"""
return "Gets information about Cristal missions"
def help_full ():
return "!cristal [id|name] : gives information about id Cristal mission."
def get_all_missions():
print (web.getContent(CONF.getNode("server")["url"]))
response = web.getXML(CONF.getNode("server")["url"])
print (CONF.getNode("server")["url"])
if response is not None:
return response.getNodes("mission")
else:
return None
def get_mission(id=None, name=None, people=None):
missions = get_all_missions()
if missions is not None:
for m in missions.childs:
if id is not None and m.getFirstNode("id").getContent() == id:
return m
elif (name is not None or name in m.getFirstNode("title").getContent()) and (people is not None or people in m.getFirstNode("contact").getContent()):
return m
return None
def cmd_cristal(msg):
if len(msg.cmds) > 1:
srch = msg.cmds[1]
else:
srch = ""
res = Response(msg.sender, channel=msg.channel, nomore="Je n'ai pas d'autre mission à afficher")
try:
id=int(srch)
name=""
except:
id=None
name=srch
missions = get_all_missions()
if missions is not None:
print (missions)
for m in missions:
print (m)
idm = m.getFirstNode("id").getContent()
crs = m.getFirstNode("title").getContent()
contact = m.getFirstNode("contact").getDate()
updated = m.getFirstNode("updated").getDate()
content = m.getFirstNode("content").getContent()
res.append_message(msg, crs + " ; contacter : " + contact + " : " + content)
else:
res.append_message("Aucune mission n'a été trouvée")
return res

View file

@ -1,5 +0,0 @@
<?xml version="1.0" ?>
<nemubotmodule name="cristal">
<server url="http://p0m.fr/cristal.php?f=xml" />
<message type="cmd" name="cristal" call="cmd_cristal" />
</nemubotmodule>

32
modules/ctfs.py Normal file
View file

@ -0,0 +1,32 @@
"""List upcoming CTFs"""
# PYTHON STUFFS #######################################################
from bs4 import BeautifulSoup
from nemubot.hooks import hook
from nemubot.tools.web import getURLContent, striphtml
from nemubot.module.more import Response
# GLOBALS #############################################################
URL = 'https://ctftime.org/event/list/upcoming'
# MODULE INTERFACE ####################################################
@hook.command("ctfs",
help="Display the upcoming CTFs")
def get_info_yt(msg):
soup = BeautifulSoup(getURLContent(URL))
res = Response(channel=msg.channel, nomore="No more upcoming CTF")
for line in soup.body.find_all('tr'):
n = line.find_all('td')
if len(n) == 7:
res.append_message("\x02%s:\x0F from %s type %s at %s. Weight: %s. %s%s" %
tuple([striphtml(x.text).strip() for x in n]))
return res

99
modules/cve.py Normal file
View file

@ -0,0 +1,99 @@
"""Read CVE in your IM client"""
# PYTHON STUFFS #######################################################
from bs4 import BeautifulSoup
from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools.web import getURLContent, striphtml
from nemubot.module.more import Response
BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/'
# MODULE CORE #########################################################
VULN_DATAS = {
"alert-title": "vuln-warning-status-name",
"alert-content": "vuln-warning-banner-content",
"description": "vuln-description",
"published": "vuln-published-on",
"last_modified": "vuln-last-modified-on",
"base_score": "vuln-cvssv3-base-score-link",
"severity": "vuln-cvssv3-base-score-severity",
"impact_score": "vuln-cvssv3-impact-score",
"exploitability_score": "vuln-cvssv3-exploitability-score",
"av": "vuln-cvssv3-av",
"ac": "vuln-cvssv3-ac",
"pr": "vuln-cvssv3-pr",
"ui": "vuln-cvssv3-ui",
"s": "vuln-cvssv3-s",
"c": "vuln-cvssv3-c",
"i": "vuln-cvssv3-i",
"a": "vuln-cvssv3-a",
}
def get_cve(cve_id):
search_url = BASEURL_NIST + quote(cve_id.upper())
soup = BeautifulSoup(getURLContent(search_url))
vuln = {}
for vd in VULN_DATAS:
r = soup.body.find(attrs={"data-testid": VULN_DATAS[vd]})
if r:
vuln[vd] = r.text.strip()
return vuln
def display_metrics(av, ac, pr, ui, s, c, i, a, **kwargs):
ret = []
if av != "None": ret.append("Attack Vector: \x02%s\x0F" % av)
if ac != "None": ret.append("Attack Complexity: \x02%s\x0F" % ac)
if pr != "None": ret.append("Privileges Required: \x02%s\x0F" % pr)
if ui != "None": ret.append("User Interaction: \x02%s\x0F" % ui)
if s != "Unchanged": ret.append("Scope: \x02%s\x0F" % s)
if c != "None": ret.append("Confidentiality: \x02%s\x0F" % c)
if i != "None": ret.append("Integrity: \x02%s\x0F" % i)
if a != "None": ret.append("Availability: \x02%s\x0F" % a)
return ', '.join(ret)
# MODULE INTERFACE ####################################################
@hook.command("cve",
help="Display given CVE",
help_usage={"CVE_ID": "Display the description of the given CVE"})
def get_cve_desc(msg):
res = Response(channel=msg.channel)
for cve_id in msg.args:
if cve_id[:3].lower() != 'cve':
cve_id = 'cve-' + cve_id
cve = get_cve(cve_id)
if not cve:
raise IMException("CVE %s doesn't exists." % cve_id)
if "alert-title" in cve or "alert-content" in cve:
alert = "\x02%s:\x0F %s " % (cve["alert-title"] if "alert-title" in cve else "",
cve["alert-content"] if "alert-content" in cve else "")
else:
alert = ""
if "base_score" not in cve and "description" in cve:
res.append_message("{alert}Last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, **cve), title=cve_id)
else:
metrics = display_metrics(**cve)
res.append_message("{alert}Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id)
return res

138
modules/ddg.py Normal file
View file

@ -0,0 +1,138 @@
"""Search around DuckDuckGo search engine"""
# PYTHON STUFFS #######################################################
from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
# MODULE CORE #########################################################
def do_search(terms):
if "!safeoff" in terms:
terms.remove("!safeoff")
safeoff = True
else:
safeoff = False
sterm = " ".join(terms)
return DDGResult(sterm, web.getJSON(
"https://api.duckduckgo.com/?q=%s&format=json&no_redirect=1%s" %
(quote(sterm), "&kp=-1" if safeoff else "")))
class DDGResult:
def __init__(self, terms, res):
if res is None:
raise IMException("An error occurs during search")
self.terms = terms
self.ddgres = res
@property
def type(self):
if not self.ddgres or "Type" not in self.ddgres:
return ""
return self.ddgres["Type"]
@property
def definition(self):
if "Definition" not in self.ddgres or not self.ddgres["Definition"]:
return None
return self.ddgres["Definition"] + " <" + self.ddgres["DefinitionURL"] + "> from " + self.ddgres["DefinitionSource"]
@property
def relatedTopics(self):
if "RelatedTopics" in self.ddgres:
for rt in self.ddgres["RelatedTopics"]:
if "Text" in rt:
yield rt["Text"] + " <" + rt["FirstURL"] + ">"
elif "Topics" in rt:
yield rt["Name"] + ": " + "; ".join([srt["Text"] + " <" + srt["FirstURL"] + ">" for srt in rt["Topics"]])
@property
def redirect(self):
if "Redirect" not in self.ddgres or not self.ddgres["Redirect"]:
return None
return self.ddgres["Redirect"]