Compare commits
259 commits
feature/ev
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 27bd0c50c1 | |||
| 9d7c278d1a | |||
| 310f933091 | |||
| 26282cb81d | |||
| de2c37a54a | |||
| 622159f6b5 | |||
| 32ebe42f41 | |||
| ea0ec42a4b | |||
| 23f043673f | |||
| ac432fabcc | |||
| 38b5b1eabd | |||
| 84fef789b5 | |||
| 45a27b477d | |||
| a8472ecc29 | |||
| 861ca0afdd | |||
| 9f83e5b178 | |||
| 68c61f40d3 | |||
| 60a9ec92b7 | |||
| 8dd6b9d471 | |||
| 13c643fc19 | |||
| 5ec2f2997b | |||
| faa5759645 | |||
| 37a230e70e | |||
| 904ad72316 | |||
| f17f8b9dfa | |||
| d56c2396c0 | |||
| b369683914 | |||
| aee2da4122 | |||
| 1f2d297ea1 | |||
|
|
b72871a8c2 | ||
| 644a641b13 | |||
| 4499677d55 | |||
| 87b5ce842d | |||
| 144551a232 | |||
| 7854e8628f | |||
| 85c418bd06 | |||
| 20c19a72bc | |||
| 9417e2ba93 | |||
| 517bf21d25 | |||
| fa0f2e93ef | |||
| b349d22370 | |||
| 8605932702 | |||
| 10b8ce8940 | |||
| 445a66ea90 | |||
| 46541cb35e | |||
| 8224d11207 | |||
| 15064204d8 | |||
| 67dee382a6 | |||
| f1da640a5b | |||
| 2fd20d9002 | |||
| 31abcc97cf | |||
| 53fe00ed58 | |||
| 4a636b2b11 | |||
| 72bc8d3839 | |||
| 5578e8b86e | |||
| 3b99099b52 | |||
| cd6750154c | |||
| b8741bb1f7 | |||
| 125ae6ad0b | |||
| 015fb47d90 | |||
| 8a25ebb45b | |||
| 1887e481d2 | |||
| 342bb9acdc | |||
| 2af56e606a | |||
| 4275009dea | |||
| f520c67c89 | |||
| c3c7484792 | |||
| 4cd099e087 | |||
| d528746cb5 | |||
|
|
5cbad96492 | ||
| 226ee4e34e | |||
|
|
e0d7ef1314 | ||
|
|
99384ad6f7 | ||
|
|
ef4c119f1f | ||
|
|
a7f4ccc959 | ||
|
|
c23dc22ce2 | ||
| bb0e958118 | |||
| b15d18b3a5 | |||
| 5646850df1 | |||
| 30ec912162 | |||
| 28d4e507eb | |||
| 62cd92e1cb | |||
| 12ddf40ef4 | |||
| 7a4b27510c | |||
| 05d20ed6ee | |||
| f60de818f2 | |||
| 45fe5b2156 | |||
| e49312e63e | |||
| a11ccb2e39 | |||
| fde459c3ff | |||
| 694c54a6bc | |||
| 1dae3c713a | |||
| 89772ebce0 | |||
| aa81aa4e96 | |||
| 3c7ed176c0 | |||
| 6dda142188 | |||
| dcb44ca3f2 | |||
| 0a576410c7 | |||
| 128afb5914 | |||
| 39b7b1ae2f | |||
| 09462d0d90 | |||
| b8f4560780 | |||
| 281d81acc4 | |||
| 29817ba1c1 | |||
| b517cac4cf | |||
| f81349bbfd | |||
| ce012b7017 | |||
|
|
e3b6c3b85e | ||
| 39056cf358 | |||
| f16dedb320 | |||
| 171297b581 | |||
| 3267c3e2e1 | |||
| aad777058e | |||
| f633a3effe | |||
| bbfecdfced | |||
| 94ff951b2e | |||
| a5479d7b0d | |||
| 0a3744577d | |||
| 67cb3caa95 | |||
| 2265e1a096 | |||
| b6945cf81c | |||
| e8809b77d2 | |||
| 9d446cbd14 | |||
| cde4ee05f7 | |||
| ac0cf729f1 | |||
| 35e0890563 | |||
| 58c349eb2c | |||
| bcd57e61ea | |||
| 0be6ebcd4b | |||
| b4218478bd | |||
| 6ac9fc4857 | |||
| 8a96f7bee9 | |||
| 7791f24423 | |||
| b809451be2 | |||
| dbcc7c664f | |||
| 8de31d784b | |||
| f4216af7c7 | |||
| cf8e1cffc5 | |||
| 97a1385903 | |||
| 764e6f070b | |||
| 6d8dca211d | |||
| 1c21231f31 | |||
| 2a3cd07c63 | |||
| b5d5a67b2d | |||
| 4c11c5e215 | |||
| a0c3f6d2b3 | |||
| 7cf73fb84a | |||
| 6cd299ab60 | |||
| fc14c76b6d | |||
| 24eb9a6911 | |||
| 38fd9e5091 | |||
| a7d7013639 | |||
| 9dc385a32a | |||
| 150d069dfb | |||
| f160411f71 | |||
| b0678ceb84 | |||
| 57275f5735 | |||
| ec512fc540 | |||
| 7bc37617b0 | |||
| 1858a045cc | |||
| 1b108428c2 | |||
| 08a7701238 | |||
| be9492c151 | |||
| 679f50b730 | |||
| 2334bc502a | |||
| c3f2c89c7c | |||
| 920506c702 | |||
| aefc0bb534 | |||
| 52b3bfa945 | |||
| c8c9112b0f | |||
| 7a48dc2cef | |||
| 0efee0cb83 | |||
| 3301fb87c2 | |||
| 3f2b18cae8 | |||
| e9cea5d010 | |||
|
|
f2c44a1108 | ||
|
|
f15ebd7c02 | ||
| 91b550754f | |||
| abf810209e | |||
| 358499e6d5 | |||
| 5fae67255b | |||
| 2c3d61495f | |||
| a05821620d | |||
| 26668c81b1 | |||
| 663e5e7207 | |||
| 6ad979a5eb | |||
| 09e3b082c1 | |||
| ff6460b92e | |||
| d028afd09e | |||
| bd2eff83b7 | |||
| 645c18c981 | |||
| 1d13d56dce | |||
| 277d55d521 | |||
| d705d351c0 | |||
| 9ff8a3a02b | |||
| 1d18305870 | |||
| 009ab08821 | |||
| 313c693d48 | |||
| c9801ee2f7 | |||
| a089efff1a | |||
| d4b6283e23 | |||
| a3236cd67a | |||
| 274836e39a | |||
| 707131023a | |||
| 57c460fc9c | |||
| cd0dbc4cc2 | |||
| d59f629dd9 | |||
| 1e29061bc9 | |||
| e03d803ae0 | |||
| f47aa8c478 | |||
| 6fc6561186 | |||
| ea8656ce0d | |||
| 0ba763f8b1 | |||
| 43c42e1397 | |||
| 926648517f | |||
| 31d93734a6 | |||
| e83c4091bf | |||
| 7ae7e381c3 | |||
| f27347f028 | |||
| 38412c1c16 | |||
| 2ebd86b80f | |||
| 0f4a904a77 | |||
| 36cfdd8861 | |||
| 11bdf8d0a1 | |||
| 00fa139e54 | |||
| 1ef54426bc | |||
| 6aef54910e | |||
| a4e6e4ce84 | |||
| c4a7df7a6f | |||
| 3a1ce6c9e8 | |||
| c06fb69c8b | |||
| f39a0eac56 | |||
| 49d7e4ced6 | |||
| de2e1d6216 | |||
| ea9829b341 | |||
| 70b52d5567 | |||
| 8ff0b626a2 | |||
| 979f1d0c55 | |||
| 9790954dfc | |||
| c6aa38147b | |||
| 8b4f08c5bd | |||
| ac33ceb579 | |||
| 9935e038fc | |||
| f496c31d1c | |||
|
|
c6e1e9acb2 | ||
|
|
1e36846265 | ||
|
|
04d5be04fa | ||
|
|
3cb9a54cee | ||
|
|
497263eaf7 | ||
| e4d67ec345 | |||
| 2fdef0afe4 | |||
| c560e13f24 | |||
| 92530ef1b2 | |||
| 59ea2e971b | |||
| 2b96c32063 | |||
|
|
aca073faff | ||
|
|
7ce9b2bb4c | ||
| 5141a0dc17 | |||
| 56c43179f3 |
138 changed files with 6680 additions and 4053 deletions
26
.drone.yml
Normal file
26
.drone.yml
Normal 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
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "modules/nextstop/external"]
|
||||
path = modules/nextstop/external
|
||||
url = git://github.com/nbr23/NextStop.git
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
language: python
|
||||
python:
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
- nightly
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
|
|
|
|||
21
Dockerfile
Normal file
21
Dockerfile
Normal 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" ]
|
||||
43
README.md
43
README.md
|
|
@ -1,17 +1,50 @@
|
|||
# *nemubot*
|
||||
nemubot
|
||||
=======
|
||||
|
||||
An extremely modulable IRC bot, built around XML configuration files!
|
||||
|
||||
|
||||
## Requirements
|
||||
Requirements
|
||||
------------
|
||||
|
||||
*nemubot* requires at least Python 3.3 to work.
|
||||
|
||||
Some modules (like `cve`, `nextstop` or `laposte`) require the
|
||||
[BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/),
|
||||
[BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/),
|
||||
but the core and framework has no dependency.
|
||||
|
||||
|
||||
## Documentation
|
||||
Installation
|
||||
------------
|
||||
|
||||
Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
#!/usr/bin/env python3.3
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone">
|
||||
|
||||
<server host="irc.rezosup.org" port="6667" autoconnect="true" caps="znc.in/server-time-iso">
|
||||
<server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso">
|
||||
<channel name="#nemutest" />
|
||||
</server>
|
||||
|
||||
<!--
|
||||
<server host="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on">
|
||||
<server host="ircs://my_host.local:6667" password="secret" autoconnect="true">
|
||||
<channel name="#nemutest" />
|
||||
</server>
|
||||
-->
|
||||
|
|
|
|||
122
modules/alias.py
122
modules/alias.py
|
|
@ -3,17 +3,16 @@
|
|||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
import shlex
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
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 more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
|
@ -77,7 +76,7 @@ def get_variable(name, msg=None):
|
|||
elif name in context.data.getNode("variables").index:
|
||||
return context.data.getNode("variables").index[name]["value"]
|
||||
else:
|
||||
return ""
|
||||
return None
|
||||
|
||||
|
||||
def list_variables(user=None):
|
||||
|
|
@ -109,12 +108,12 @@ def set_variable(name, value, creator):
|
|||
context.save()
|
||||
|
||||
|
||||
def replace_variables(cnts, msg=None):
|
||||
def replace_variables(cnts, msg):
|
||||
"""Replace variables contained in the content
|
||||
|
||||
Arguments:
|
||||
cnt -- content where search variables
|
||||
msg -- optional message where pick some variables
|
||||
msg -- Message where pick some variables
|
||||
"""
|
||||
|
||||
unsetCnt = list()
|
||||
|
|
@ -123,12 +122,12 @@ def replace_variables(cnts, msg=None):
|
|||
resultCnt = list()
|
||||
|
||||
for cnt in cnts:
|
||||
for res in re.findall("\\$\{(?P<name>[a-zA-Z0-9:]+)\}", cnt):
|
||||
rv = re.match("([0-9]+)(:([0-9]*))?", res)
|
||||
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, "", 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
|
||||
|
|
@ -143,11 +142,12 @@ def replace_variables(cnts, msg=None):
|
|||
cnt = cnt.replace("${%s}" % res, msg.args[varI], 1)
|
||||
unsetCnt.append(varI)
|
||||
else:
|
||||
cnt = cnt.replace("${%s}" % res, get_variable(res), 1)
|
||||
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):
|
||||
k = msg.args.pop(u)
|
||||
msg.args.pop(u)
|
||||
|
||||
return resultCnt
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ def replace_variables(cnts, msg=None):
|
|||
|
||||
## Variables management
|
||||
|
||||
@hook("cmd_hook", "listvars",
|
||||
@hook.command("listvars",
|
||||
help="list defined variables for substitution in input commands",
|
||||
help_usage={
|
||||
None: "List all known variables",
|
||||
|
|
@ -179,20 +179,20 @@ def cmd_listvars(msg):
|
|||
return Response("There is currently no variable stored.", channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_hook", "set",
|
||||
@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 IRCException("!set take two args: the key and the value.")
|
||||
set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick)
|
||||
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("cmd_hook", "listalias",
|
||||
@hook.command("listalias",
|
||||
help="List registered aliases",
|
||||
help_usage={
|
||||
None: "List all registered aliases",
|
||||
|
|
@ -206,27 +206,42 @@ def cmd_listalias(msg):
|
|||
return Response("There is no alias currently.", channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_hook", "alias",
|
||||
help="Display the replacement command for a given alias")
|
||||
@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 IRCException("!alias takes as argument an alias to extend.")
|
||||
res = list()
|
||||
for alias in msg.args:
|
||||
if alias[0] == "!":
|
||||
alias = alias[1:]
|
||||
if alias in context.data.getNode("aliases").index:
|
||||
res.append("!%s correspond to %s" % (alias, context.data.getNode("aliases").index[alias]["origin"]))
|
||||
else:
|
||||
res.append("!%s is not an alias" % alias)
|
||||
return Response(res, channel=msg.channel, nick=msg.nick)
|
||||
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("cmd_hook", "unalias",
|
||||
@hook.command("unalias",
|
||||
help="Remove a previously created alias")
|
||||
def cmd_unalias(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Which alias would you want to remove?")
|
||||
raise IMException("Which alias would you want to remove?")
|
||||
res = list()
|
||||
for alias in msg.args:
|
||||
if alias[0] == "!" and len(alias) > 1:
|
||||
|
|
@ -243,39 +258,20 @@ def cmd_unalias(msg):
|
|||
|
||||
## Alias replacement
|
||||
|
||||
@hook("pre_Command")
|
||||
@hook.add(["pre","Command"])
|
||||
def treat_alias(msg):
|
||||
if msg.cmd in context.data.getNode("aliases").index:
|
||||
txt = context.data.getNode("aliases").index[msg.cmd]["origin"]
|
||||
# TODO: for legacy compatibility
|
||||
if txt[0] == "!":
|
||||
txt = txt[1:]
|
||||
try:
|
||||
args = shlex.split(txt)
|
||||
except ValueError:
|
||||
args = txt.split(' ')
|
||||
nmsg = Command(args[0], replace_variables(args[1:], msg) + msg.args, **msg.export_args())
|
||||
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 msg.cmd != nmsg.cmd:
|
||||
# Also return origin message, if it can be treated as well
|
||||
return [msg, nmsg]
|
||||
if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd:
|
||||
return rpl_msg
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
@hook("ask_default")
|
||||
def parseask(msg):
|
||||
if re.match(".*(register|set|cr[ée]{2}|new|nouvel(le)?) alias.*", msg.text) is not None:
|
||||
result = re.match(".*alias !?([^ ]+) ?(pour|for|=|:) ?(.+)$", msg.text)
|
||||
if result.group(1) in context.data.getNode("aliases").index:
|
||||
raise IRCException("this alias is already defined.")
|
||||
else:
|
||||
create_alias(result.group(1),
|
||||
result.group(3),
|
||||
channel=msg.channel,
|
||||
creator=msg.nick)
|
||||
res = Response("New alias %s successfully registered." %
|
||||
result.group(1), channel=msg.channel)
|
||||
return res
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import sys
|
|||
from datetime import date, datetime
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
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
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
|
@ -27,7 +27,7 @@ def load(context):
|
|||
def findName(msg):
|
||||
if (not len(msg.args) or msg.args[0].lower() == "moi" or
|
||||
msg.args[0].lower() == "me"):
|
||||
name = msg.nick.lower()
|
||||
name = msg.frm.lower()
|
||||
else:
|
||||
name = msg.args[0].lower()
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ def findName(msg):
|
|||
|
||||
## Commands
|
||||
|
||||
@hook("cmd_hook", "anniv",
|
||||
@hook.command("anniv",
|
||||
help="gives the remaining time before the anniversary of known people",
|
||||
help_usage={
|
||||
None: "Calculate the time remaining before your birthday",
|
||||
|
|
@ -77,10 +77,10 @@ def cmd_anniv(msg):
|
|||
else:
|
||||
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("cmd_hook", "age",
|
||||
@hook.command("age",
|
||||
help="Calculate age of known people",
|
||||
help_usage={
|
||||
None: "Calculate your age",
|
||||
|
|
@ -98,26 +98,26 @@ def cmd_age(msg):
|
|||
msg.channel)
|
||||
else:
|
||||
return Response("désolé, je ne connais pas l'âge de %s."
|
||||
" Quand est-il né ?" % name, msg.channel, msg.nick)
|
||||
" Quand est-il né ?" % name, msg.channel, msg.frm)
|
||||
return True
|
||||
|
||||
|
||||
## Input parsing
|
||||
|
||||
@hook("ask_default")
|
||||
@hook.ask()
|
||||
def parseask(msg):
|
||||
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.text, re.I)
|
||||
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 = extractDate(msg.text)
|
||||
extDate = extractDate(msg.message)
|
||||
if extDate is None or extDate.year > datetime.now().year:
|
||||
return Response("la date de naissance ne paraît pas valide...",
|
||||
msg.channel,
|
||||
msg.nick)
|
||||
msg.frm)
|
||||
else:
|
||||
nick = res.group(1)
|
||||
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
|
||||
nick = msg.nick
|
||||
nick = msg.frm
|
||||
if nick.lower() in context.data.index:
|
||||
context.data.index[nick.lower()]["born"] = extDate
|
||||
else:
|
||||
|
|
@ -129,6 +129,6 @@ def parseask(msg):
|
|||
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:
|
||||
raise IRCException("la date de naissance ne paraît pas valide.")
|
||||
raise IMException("la date de naissance ne paraît pas valide.")
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@
|
|||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.event import ModuleEvent
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.countdown import countdown_format
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
|
@ -47,9 +46,9 @@ def load(context):
|
|||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook("cmd_hook", "newyear",
|
||||
@hook.command("newyear",
|
||||
help="Display the remaining time before the next new year")
|
||||
@hook("cmd_hook", str(yrn),
|
||||
@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,
|
||||
|
|
@ -59,7 +58,7 @@ def cmd_newyear(msg):
|
|||
channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$",
|
||||
@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.cmd)
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@
|
|||
import urllib
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
if not context.config or not context.config.getAttribute("goodreadskey"):
|
||||
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"
|
||||
|
|
@ -28,8 +28,8 @@ 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 response.hasNode("book"):
|
||||
return response.getNode("book")
|
||||
if response is not None and len(response.getElementsByTagName("book")):
|
||||
return response.getElementsByTagName("book")[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
@ -38,8 +38,8 @@ 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 response.hasNode("search"):
|
||||
return response.getNode("search").getNode("results").getNodes("work")
|
||||
if response is not None and len(response.getElementsByTagName("search")):
|
||||
return response.getElementsByTagName("search")[0].getElementsByTagName("results")[0].getElementsByTagName("work")
|
||||
else:
|
||||
return []
|
||||
|
||||
|
|
@ -48,43 +48,43 @@ 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 response.hasNode("author") and response.getNode("author").hasAttribute("id"):
|
||||
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.getNode("author")["id"]), context.config["goodreadskey"]))
|
||||
if response is not None and response.hasNode("author"):
|
||||
return response.getNode("author")
|
||||
(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("cmd_hook", "book",
|
||||
@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 IRCException("please give me a title to search")
|
||||
raise IMException("please give me a title to search")
|
||||
|
||||
book = get_book(" ".join(msg.args))
|
||||
if book is None:
|
||||
raise IRCException("unable to find book named like this")
|
||||
raise IMException("unable to find book named like this")
|
||||
res = Response(channel=msg.channel)
|
||||
res.append_message("%s, writed by %s: %s" % (book.getNode("title").getContent(),
|
||||
book.getNode("authors").getNode("author").getNode("name").getContent(),
|
||||
web.striphtml(book.getNode("description").getContent())))
|
||||
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("cmd_hook", "search_books",
|
||||
@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 IRCException("please give me a title to search")
|
||||
raise IMException("please give me a title to search")
|
||||
|
||||
title = " ".join(msg.args)
|
||||
res = Response(channel=msg.channel,
|
||||
|
|
@ -92,21 +92,24 @@ def cmd_books(msg):
|
|||
count=" (%d more books)")
|
||||
|
||||
for book in search_books(title):
|
||||
res.append_message("%s, writed by %s" % (book.getNode("best_book").getNode("title").getContent(),
|
||||
book.getNode("best_book").getNode("author").getNode("name").getContent()))
|
||||
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("cmd_hook", "author_books",
|
||||
@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 IRCException("please give me an author to search")
|
||||
raise IMException("please give me an author to search")
|
||||
|
||||
ath = search_author(" ".join(msg.args))
|
||||
return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")],
|
||||
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.getNode("name").getContent())
|
||||
title=ath.getElementsByTagName("name")[0].firstChild.nodeValue)
|
||||
|
|
|
|||
55
modules/cat.py
Normal file
55
modules/cat.py
Normal 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
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 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 traceback
|
||||
import sys
|
||||
|
||||
from nemubot.hooks import hook
|
||||
|
||||
nemubotversion = 3.4
|
||||
NODATA = True
|
||||
|
||||
|
||||
def getserver(toks, context, prompt, mandatory=False, **kwargs):
|
||||
"""Choose the server in toks or prompt.
|
||||
This function modify the tokens list passed as argument"""
|
||||
|
||||
if len(toks) > 1 and toks[1] in context.servers:
|
||||
return context.servers[toks.pop(1)]
|
||||
elif not mandatory or prompt.selectedServer:
|
||||
return prompt.selectedServer
|
||||
else:
|
||||
from nemubot.prompt.error import PromptError
|
||||
raise PromptError("Please SELECT a server or give its name in argument.")
|
||||
|
||||
|
||||
@hook("prompt_cmd", "close")
|
||||
def close(toks, context, **kwargs):
|
||||
"""Disconnect and forget (remove from the servers list) the server"""
|
||||
srv = getserver(toks, context=context, mandatory=True, **kwargs)
|
||||
|
||||
if srv.close():
|
||||
del context.servers[srv.id]
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
@hook("prompt_cmd", "connect")
|
||||
def connect(toks, **kwargs):
|
||||
"""Make the connexion to a server"""
|
||||
srv = getserver(toks, mandatory=True, **kwargs)
|
||||
|
||||
return not srv.open()
|
||||
|
||||
|
||||
@hook("prompt_cmd", "disconnect")
|
||||
def disconnect(toks, **kwargs):
|
||||
"""Close the connection to a server"""
|
||||
srv = getserver(toks, mandatory=True, **kwargs)
|
||||
|
||||
return not srv.close()
|
||||
|
||||
|
||||
@hook("prompt_cmd", "discover")
|
||||
def discover(toks, context, **kwargs):
|
||||
"""Discover a new bot on a server"""
|
||||
srv = getserver(toks, context=context, mandatory=True, **kwargs)
|
||||
|
||||
if len(toks) > 1 and "!" in toks[1]:
|
||||
bot = context.add_networkbot(srv, name)
|
||||
return not bot.connect()
|
||||
else:
|
||||
print(" %s is not a valid fullname, for example: "
|
||||
"nemubot!nemubotV3@bot.nemunai.re" % ''.join(toks[1:1]))
|
||||
return 1
|
||||
|
||||
|
||||
@hook("prompt_cmd", "join")
|
||||
@hook("prompt_cmd", "leave")
|
||||
@hook("prompt_cmd", "part")
|
||||
def join(toks, **kwargs):
|
||||
"""Join or leave a channel"""
|
||||
srv = getserver(toks, mandatory=True, **kwargs)
|
||||
|
||||
if len(toks) <= 2:
|
||||
print("%s: not enough arguments." % toks[0])
|
||||
return 1
|
||||
|
||||
if toks[0] == "join":
|
||||
if len(toks) > 2:
|
||||
srv.write("JOIN %s %s" % (toks[1], toks[2]))
|
||||
else:
|
||||
srv.write("JOIN %s" % toks[1])
|
||||
|
||||
elif toks[0] == "leave" or toks[0] == "part":
|
||||
if len(toks) > 2:
|
||||
srv.write("PART %s :%s" % (toks[1], " ".join(toks[2:])))
|
||||
else:
|
||||
srv.write("PART %s" % toks[1])
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@hook("prompt_cmd", "save")
|
||||
def save_mod(toks, context, **kwargs):
|
||||
"""Force save module data"""
|
||||
if len(toks) < 2:
|
||||
print("save: not enough arguments.")
|
||||
return 1
|
||||
|
||||
wrn = 0
|
||||
for mod in toks[1:]:
|
||||
if mod in context.modules:
|
||||
context.modules[mod].save()
|
||||
print("save: module `%s´ saved successfully" % mod)
|
||||
else:
|
||||
wrn += 1
|
||||
print("save: no module named `%s´" % mod)
|
||||
return wrn
|
||||
|
||||
|
||||
@hook("prompt_cmd", "send")
|
||||
def send(toks, **kwargs):
|
||||
"""Send a message on a channel"""
|
||||
srv = getserver(toks, mandatory=True, **kwargs)
|
||||
|
||||
# Check the server is connected
|
||||
if not srv.connected:
|
||||
print ("send: server `%s' not connected." % srv.id)
|
||||
return 2
|
||||
|
||||
if len(toks) <= 3:
|
||||
print ("send: not enough arguments.")
|
||||
return 1
|
||||
|
||||
if toks[1] not in srv.channels:
|
||||
print ("send: channel `%s' not authorized in server `%s'."
|
||||
% (toks[1], srv.id))
|
||||
return 3
|
||||
|
||||
from nemubot.message import Text
|
||||
srv.send_response(Text(" ".join(toks[2:]), server=None,
|
||||
to=[toks[1]]))
|
||||
return 0
|
||||
|
||||
|
||||
@hook("prompt_cmd", "zap")
|
||||
def zap(toks, **kwargs):
|
||||
"""Hard change connexion state"""
|
||||
srv = getserver(toks, mandatory=True, **kwargs)
|
||||
|
||||
srv.connected = not srv.connected
|
||||
|
||||
|
||||
@hook("prompt_cmd", "top")
|
||||
def top(toks, context, **kwargs):
|
||||
"""Display consumers load information"""
|
||||
print("Queue size: %d, %d thread(s) running (counter: %d)" %
|
||||
(context.cnsr_queue.qsize(),
|
||||
len(context.cnsr_thrd),
|
||||
context.cnsr_thrd_size))
|
||||
if len(context.events) > 0:
|
||||
print("Events registered: %d, next in %d seconds" %
|
||||
(len(context.events),
|
||||
context.events[0].time_left.seconds))
|
||||
else:
|
||||
print("No events registered")
|
||||
|
||||
for th in context.cnsr_thrd:
|
||||
if th.is_alive():
|
||||
print(("#" * 15 + " Stack trace for thread %u " + "#" * 15) %
|
||||
th.ident)
|
||||
traceback.print_stack(sys._current_frames()[th.ident])
|
||||
|
||||
|
||||
@hook("prompt_cmd", "netstat")
|
||||
def netstat(toks, context, **kwargs):
|
||||
"""Display sockets in use and many other things"""
|
||||
if len(context.network) > 0:
|
||||
print("Distant bots connected: %d:" % len(context.network))
|
||||
for name, bot in context.network.items():
|
||||
print("# %s:" % name)
|
||||
print(" * Declared hooks:")
|
||||
lvl = 0
|
||||
for hlvl in bot.hooks:
|
||||
lvl += 1
|
||||
for hook in (hlvl.all_pre + hlvl.all_post + hlvl.cmd_rgxp +
|
||||
hlvl.cmd_default + hlvl.ask_rgxp +
|
||||
hlvl.ask_default + hlvl.msg_rgxp +
|
||||
hlvl.msg_default):
|
||||
print(" %s- %s" % (' ' * lvl * 2, hook))
|
||||
for kind in ["irc_hook", "cmd_hook", "ask_hook", "msg_hook"]:
|
||||
print(" %s- <%s> %s" % (' ' * lvl * 2, kind,
|
||||
", ".join(hlvl.__dict__[kind].keys())))
|
||||
print(" * My tag: %d" % bot.my_tag)
|
||||
print(" * Tags in use (%d):" % bot.inc_tag)
|
||||
for tag, (cmd, data) in bot.tags.items():
|
||||
print(" - %11s: %s « %s »" % (tag, cmd, data))
|
||||
else:
|
||||
print("No distant bot connected")
|
||||
|
|
@ -6,12 +6,12 @@ from collections import defaultdict
|
|||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
from nemubot.tools.web import striphtml
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
|
@ -36,7 +36,7 @@ for k, v in s:
|
|||
# MODULE CORE #########################################################
|
||||
|
||||
def get_conjug(verb, stringTens):
|
||||
url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
|
||||
url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
|
||||
quote(verb.encode("ISO-8859-1")))
|
||||
page = web.getURLContent(url)
|
||||
|
||||
|
|
@ -51,10 +51,10 @@ def compute_line(line, stringTens):
|
|||
try:
|
||||
idTemps = d[stringTens]
|
||||
except:
|
||||
raise IRCException("le temps demandé n'existe pas")
|
||||
raise IMException("le temps demandé n'existe pas")
|
||||
|
||||
if len(idTemps) == 0:
|
||||
raise IRCException("le temps demandé n'existe pas")
|
||||
raise IMException("le temps demandé n'existe pas")
|
||||
|
||||
index = line.index('<div id="temps' + idTemps[0] + '\"')
|
||||
endIndex = line[index:].index('<div class=\"conjugBloc\"')
|
||||
|
|
@ -72,13 +72,13 @@ def compute_line(line, stringTens):
|
|||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook("cmd_hook", "conjugaison",
|
||||
@hook.command("conjugaison",
|
||||
help_usage={
|
||||
"TENS VERB": "give the conjugaison for VERB in TENS."
|
||||
})
|
||||
def cmd_conjug(msg):
|
||||
if len(msg.args) < 2:
|
||||
raise IRCException("donne moi un temps et un verbe, et je te donnerai "
|
||||
raise IMException("donne moi un temps et un verbe, et je te donnerai "
|
||||
"sa conjugaison!")
|
||||
|
||||
tens = ' '.join(msg.args[:-1])
|
||||
|
|
@ -91,4 +91,4 @@ def cmd_conjug(msg):
|
|||
return Response(conjug, channel=msg.channel,
|
||||
title="Conjugaison de %s" % verb)
|
||||
else:
|
||||
raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb)
|
||||
raise IMException("aucune conjugaison de '%s' n'a été trouvé" % verb)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from bs4 import BeautifulSoup
|
|||
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.web import getURLContent, striphtml
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
|
@ -16,7 +16,7 @@ URL = 'https://ctftime.org/event/list/upcoming'
|
|||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook("cmd_hook", "ctfs",
|
||||
@hook.command("ctfs",
|
||||
help="Display the upcoming CTFs")
|
||||
def get_info_yt(msg):
|
||||
soup = BeautifulSoup(getURLContent(URL))
|
||||
|
|
@ -25,10 +25,8 @@ def get_info_yt(msg):
|
|||
|
||||
for line in soup.body.find_all('tr'):
|
||||
n = line.find_all('td')
|
||||
if len(n) == 5:
|
||||
try:
|
||||
res.append_message("\x02%s:\x0F from %s type %s at %s. %s" %
|
||||
tuple([striphtml(x.text) for x in n]))
|
||||
except:
|
||||
pass
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,26 +1,78 @@
|
|||
"""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
|
||||
from more import Response
|
||||
from nemubot.tools.web import getURLContent, striphtml
|
||||
|
||||
"""CVE description"""
|
||||
from nemubot.module.more import Response
|
||||
|
||||
nemubotversion = 4.0
|
||||
BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/'
|
||||
|
||||
BASEURL_MITRE = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name='
|
||||
|
||||
# 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_MITRE + quote(cve_id.upper())
|
||||
search_url = BASEURL_NIST + quote(cve_id.upper())
|
||||
|
||||
soup = BeautifulSoup(getURLContent(search_url))
|
||||
desc = soup.body.findAll('td')
|
||||
|
||||
return desc[17].text.replace("\n", " ") + " Moar at " + search_url
|
||||
vuln = {}
|
||||
|
||||
@hook("cmd_hook", "cve")
|
||||
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)
|
||||
|
||||
|
|
@ -28,6 +80,20 @@ def get_cve_desc(msg):
|
|||
if cve_id[:3].lower() != 'cve':
|
||||
cve_id = 'cve-' + cve_id
|
||||
|
||||
res.append_message(get_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
138
modules/ddg.py
Normal 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"]
|
||||
|
||||
|
||||
@property
|
||||
def entity(self):
|
||||
if "Entity" not in self.ddgres or not self.ddgres["Entity"]:
|
||||
return None
|
||||
return self.ddgres["Entity"]
|
||||
|
||||
|
||||
@property
|
||||
def heading(self):
|
||||
if "Heading" not in self.ddgres or not self.ddgres["Heading"]:
|
||||
return " ".join(self.terms)
|
||||
return self.ddgres["Heading"]
|
||||
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
if "Results" in self.ddgres:
|
||||
for res in self.ddgres["Results"]:
|
||||
yield res["Text"] + " <" + res["FirstURL"] + ">"
|
||||
|
||||
|
||||
@property
|
||||
def answer(self):
|
||||
if "Answer" not in self.ddgres or not self.ddgres["Answer"]:
|
||||
return None
|
||||
return web.striphtml(self.ddgres["Answer"])
|
||||
|
||||
|
||||
@property
|
||||
def abstract(self):
|
||||
if "Abstract" not in self.ddgres or not self.ddgres["Abstract"]:
|
||||
return None
|
||||
return self.ddgres["AbstractText"] + " <" + self.ddgres["AbstractURL"] + "> from " + self.ddgres["AbstractSource"]
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("define")
|
||||
def define(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("Indicate a term to define")
|
||||
|
||||
s = do_search(msg.args)
|
||||
|
||||
if not s.definition:
|
||||
raise IMException("no definition found for '%s'." % " ".join(msg.args))
|
||||
|
||||
return Response(s.definition, channel=msg.channel)
|
||||
|
||||
@hook.command("search")
|
||||
def search(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("Indicate a term to search")
|
||||
|
||||
s = do_search(msg.args)
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more results",
|
||||
count=" (%d more results)")
|
||||
|
||||
res.append_message(s.redirect)
|
||||
res.append_message(s.answer)
|
||||
res.append_message(s.abstract)
|
||||
res.append_message([r for r in s.result])
|
||||
|
||||
for rt in s.relatedTopics:
|
||||
res.append_message(rt)
|
||||
|
||||
res.append_message(s.definition)
|
||||
|
||||
return res
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.tools import web
|
||||
from nemubot.tools.xmlparser import parse_string
|
||||
|
||||
|
||||
class DDGSearch:
|
||||
|
||||
def __init__(self, terms, safeoff=False):
|
||||
self.terms = terms
|
||||
|
||||
self.ddgres = web.getXML(
|
||||
"https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1%s" %
|
||||
(quote(terms), "&kp=-1" if safeoff else ""),
|
||||
timeout=10)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
if self.ddgres and self.ddgres.hasNode("Type"):
|
||||
return self.ddgres.getFirstNode("Type").getContent()
|
||||
else:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def definition(self):
|
||||
if self.ddgres.hasNode("Definition"):
|
||||
return self.ddgres.getFirstNode("Definition").getContent()
|
||||
else:
|
||||
return "Sorry, no definition found for %s" % self.terms
|
||||
|
||||
@property
|
||||
def relatedTopics(self):
|
||||
try:
|
||||
for rt in self.ddgres.getFirstNode("RelatedTopics").getNodes("RelatedTopic"):
|
||||
yield rt.getFirstNode("Text").getContent()
|
||||
except:
|
||||
pass
|
||||
|
||||
@property
|
||||
def redirect(self):
|
||||
try:
|
||||
return self.ddgres.getFirstNode("Redirect").getContent()
|
||||
except:
|
||||
return None
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
try:
|
||||
node = self.ddgres.getFirstNode("Results").getFirstNode("Result")
|
||||
return node.getFirstNode("Text").getContent() + ": " + node.getFirstNode("FirstURL").getContent()
|
||||
except:
|
||||
return None
|
||||
|
||||
@property
|
||||
def answer(self):
|
||||
try:
|
||||
return web.striphtml(self.ddgres.getFirstNode("Answer").getContent())
|
||||
except:
|
||||
return None
|
||||
|
||||
@property
|
||||
def abstract(self):
|
||||
try:
|
||||
if self.ddgres.getNode("Abstract").getContent() != "":
|
||||
return self.ddgres.getNode("Abstract").getContent() + " <" + self.ddgres.getNode("AbstractURL").getContent() + ">"
|
||||
else:
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.tools import web
|
||||
|
||||
|
||||
class UrbanDictionnary:
|
||||
|
||||
def __init__(self, terms):
|
||||
self.terms = terms
|
||||
|
||||
self.udres = web.getJSON(
|
||||
"http://api.urbandictionary.com/v0/define?term=%s" % quote(terms),
|
||||
timeout=10)
|
||||
|
||||
@property
|
||||
def result_type(self):
|
||||
if self.udres and "result_type" in self.udres:
|
||||
return self.udres["result_type"]
|
||||
else:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def definitions(self):
|
||||
if self.udres and "list" in self.udres:
|
||||
for d in self.udres["list"]:
|
||||
yield d["definition"] + "\n" + d["example"]
|
||||
else:
|
||||
yield "Sorry, no definition found for %s" % self.terms
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Search around various search engine or knowledges database"""
|
||||
|
||||
import imp
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.hooks import hook
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
|
||||
from . import DDGSearch
|
||||
from . import UrbanDictionnary
|
||||
|
||||
@hook("cmd_hook", "define")
|
||||
def define(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate a term to define")
|
||||
|
||||
s = DDGSearch.DDGSearch(' '.join(msg.args))
|
||||
|
||||
return Response(s.definition, channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_hook", "search")
|
||||
def search(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate a term to search")
|
||||
|
||||
if "!safeoff" in msg.args:
|
||||
msg.args.remove("!safeoff")
|
||||
safeoff = True
|
||||
else:
|
||||
safeoff = False
|
||||
|
||||
s = DDGSearch.DDGSearch(' '.join(msg.args), safeoff)
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more results",
|
||||
count=" (%d more results)")
|
||||
|
||||
res.append_message(s.redirect)
|
||||
res.append_message(s.abstract)
|
||||
res.append_message(s.result)
|
||||
res.append_message(s.answer)
|
||||
|
||||
for rt in s.relatedTopics:
|
||||
res.append_message(rt)
|
||||
|
||||
res.append_message(s.definition)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "urbandictionnary")
|
||||
def udsearch(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate a term to search")
|
||||
|
||||
s = UrbanDictionnary.UrbanDictionnary(' '.join(msg.args))
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more results",
|
||||
count=" (%d more definitions)")
|
||||
|
||||
for d in s.definitions:
|
||||
res.append_message(d.replace("\n", " "))
|
||||
|
||||
return res
|
||||
94
modules/dig.py
Normal file
94
modules/dig.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""DNS resolver"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
|
||||
import dns.exception
|
||||
import dns.name
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.resolver
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("dig",
|
||||
help="Resolve domain name with a basic syntax similar to dig(1)")
|
||||
def dig(msg):
|
||||
lclass = "IN"
|
||||
ltype = "A"
|
||||
ledns = None
|
||||
ltimeout = 6.0
|
||||
ldomain = None
|
||||
lnameservers = []
|
||||
lsearchlist = []
|
||||
loptions = []
|
||||
for a in msg.args:
|
||||
if a in dns.rdatatype._by_text:
|
||||
ltype = a
|
||||
elif a in dns.rdataclass._by_text:
|
||||
lclass = a
|
||||
elif a[0] == "@":
|
||||
try:
|
||||
lnameservers.append(str(ipaddress.ip_address(a[1:])))
|
||||
except ValueError:
|
||||
for r in socket.getaddrinfo(a[1:], 53, proto=socket.IPPROTO_UDP):
|
||||
lnameservers.append(r[4][0])
|
||||
|
||||
elif a[0:8] == "+domain=":
|
||||
lsearchlist.append(dns.name.from_unicode(a[8:]))
|
||||
elif a[0:6] == "+edns=":
|
||||
ledns = int(a[6:])
|
||||
elif a[0:6] == "+time=":
|
||||
ltimeout = float(a[6:])
|
||||
elif a[0] == "+":
|
||||
loptions.append(a[1:])
|
||||
else:
|
||||
ldomain = a
|
||||
|
||||
if not ldomain:
|
||||
raise IMException("indicate a domain to resolve")
|
||||
|
||||
resolv = dns.resolver.Resolver()
|
||||
if ledns:
|
||||
resolv.edns = ledns
|
||||
resolv.lifetime = ltimeout
|
||||
resolv.timeout = ltimeout
|
||||
resolv.flags = (
|
||||
dns.flags.QR | dns.flags.RA |
|
||||
dns.flags.AA if "aaonly" in loptions or "aaflag" in loptions else 0 |
|
||||
dns.flags.AD if "adflag" in loptions else 0 |
|
||||
dns.flags.CD if "cdflag" in loptions else 0 |
|
||||
dns.flags.RD if "norecurse" not in loptions else 0
|
||||
)
|
||||
if lsearchlist:
|
||||
resolv.search = lsearchlist
|
||||
else:
|
||||
resolv.search = [dns.name.from_text(".")]
|
||||
|
||||
if lnameservers:
|
||||
resolv.nameservers = lnameservers
|
||||
|
||||
try:
|
||||
answers = resolv.query(ldomain, ltype, lclass, tcp="tcp" in loptions)
|
||||
except dns.exception.DNSException as e:
|
||||
raise IMException(str(e))
|
||||
|
||||
res = Response(channel=msg.channel, count=" (%s others entries)")
|
||||
for rdata in answers:
|
||||
res.append_message("%s %s %s %s %s" % (
|
||||
answers.qname.to_text(),
|
||||
answers.ttl if not "nottlid" in loptions else "",
|
||||
dns.rdataclass.to_text(answers.rdclass) if not "nocl" in loptions else "",
|
||||
dns.rdatatype.to_text(answers.rdtype),
|
||||
rdata.to_text())
|
||||
)
|
||||
|
||||
return res
|
||||
89
modules/disas.py
Normal file
89
modules/disas.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""The Ultimate Disassembler Module"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import capstone
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
ARCHITECTURES = {
|
||||
"arm": capstone.CS_ARCH_ARM,
|
||||
"arm64": capstone.CS_ARCH_ARM64,
|
||||
"mips": capstone.CS_ARCH_MIPS,
|
||||
"ppc": capstone.CS_ARCH_PPC,
|
||||
"sparc": capstone.CS_ARCH_SPARC,
|
||||
"sysz": capstone.CS_ARCH_SYSZ,
|
||||
"x86": capstone.CS_ARCH_X86,
|
||||
"xcore": capstone.CS_ARCH_XCORE,
|
||||
}
|
||||
|
||||
MODES = {
|
||||
"arm": capstone.CS_MODE_ARM,
|
||||
"thumb": capstone.CS_MODE_THUMB,
|
||||
"mips32": capstone.CS_MODE_MIPS32,
|
||||
"mips64": capstone.CS_MODE_MIPS64,
|
||||
"mips32r6": capstone.CS_MODE_MIPS32R6,
|
||||
"16": capstone.CS_MODE_16,
|
||||
"32": capstone.CS_MODE_32,
|
||||
"64": capstone.CS_MODE_64,
|
||||
"le": capstone.CS_MODE_LITTLE_ENDIAN,
|
||||
"be": capstone.CS_MODE_BIG_ENDIAN,
|
||||
"micro": capstone.CS_MODE_MICRO,
|
||||
"mclass": capstone.CS_MODE_MCLASS,
|
||||
"v8": capstone.CS_MODE_V8,
|
||||
"v9": capstone.CS_MODE_V9,
|
||||
}
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("disas",
|
||||
help="Display assembly code",
|
||||
help_usage={"CODE": "Display assembly code corresponding to the given CODE"},
|
||||
keywords={
|
||||
"arch=ARCH": "Specify the architecture of the code to disassemble (default: x86, choose between: %s)" % ', '.join(ARCHITECTURES.keys()),
|
||||
"modes=MODE[,MODE]": "Specify hardware mode of the code to disassemble (default: 32, between: %s)" % ', '.join(MODES.keys()),
|
||||
})
|
||||
def cmd_disas(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("please give me some code")
|
||||
|
||||
# Determine the architecture
|
||||
if "arch" in msg.kwargs:
|
||||
if msg.kwargs["arch"] not in ARCHITECTURES:
|
||||
raise IMException("unknown architectures '%s'" % msg.kwargs["arch"])
|
||||
architecture = ARCHITECTURES[msg.kwargs["arch"]]
|
||||
else:
|
||||
architecture = capstone.CS_ARCH_X86
|
||||
|
||||
# Determine hardware modes
|
||||
modes = 0
|
||||
if "modes" in msg.kwargs:
|
||||
for mode in msg.kwargs["modes"].split(','):
|
||||
if mode not in MODES:
|
||||
raise IMException("unknown mode '%s'" % mode)
|
||||
modes += MODES[mode]
|
||||
elif architecture == capstone.CS_ARCH_X86 or architecture == capstone.CS_ARCH_PPC:
|
||||
modes = capstone.CS_MODE_32
|
||||
elif architecture == capstone.CS_ARCH_ARM or architecture == capstone.CS_ARCH_ARM64:
|
||||
modes = capstone.CS_MODE_ARM
|
||||
elif architecture == capstone.CS_ARCH_MIPS:
|
||||
modes = capstone.CS_MODE_MIPS32
|
||||
|
||||
# Get the code
|
||||
code = bytearray.fromhex(''.join([a.replace("0x", "") for a in msg.args]))
|
||||
|
||||
# Setup capstone
|
||||
md = capstone.Cs(architecture, modes)
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more instruction")
|
||||
|
||||
for isn in md.disasm(code, 0x1000):
|
||||
res.append_message("%s %s" %(isn.mnemonic, isn.op_str), title="0x%x" % isn.address)
|
||||
|
||||
return res
|
||||
|
|
@ -1,49 +1,99 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Create countdowns and reminders"""
|
||||
|
||||
import imp
|
||||
import re
|
||||
import sys
|
||||
import calendar
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import time
|
||||
import threading
|
||||
import traceback
|
||||
from functools import partial
|
||||
import re
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.event import ModuleEvent
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.message import Command
|
||||
from nemubot.tools.countdown import countdown_format, countdown
|
||||
from nemubot.tools.date import extractDate
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
from nemubot.tools.xmlparser.basic import DictNode
|
||||
|
||||
nemubotversion = 3.4
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
class Event:
|
||||
|
||||
def __init__(self, server, channel, creator, start_time, end_time=None):
|
||||
self._server = server
|
||||
self._channel = channel
|
||||
self._creator = creator
|
||||
self._start = datetime.utcfromtimestamp(float(start_time)).replace(tzinfo=timezone.utc) if not isinstance(start_time, datetime) else start_time
|
||||
self._end = datetime.utcfromtimestamp(float(end_time)).replace(tzinfo=timezone.utc) if end_time else None
|
||||
self._evt = None
|
||||
|
||||
|
||||
def __del__(self):
|
||||
if self._evt is not None:
|
||||
context.del_event(self._evt)
|
||||
self._evt = None
|
||||
|
||||
|
||||
def saveElement(self, store, tag="event"):
|
||||
attrs = {
|
||||
"server": str(self._server),
|
||||
"channel": str(self._channel),
|
||||
"creator": str(self._creator),
|
||||
"start_time": str(calendar.timegm(self._start.timetuple())),
|
||||
}
|
||||
if self._end:
|
||||
attrs["end_time"] = str(calendar.timegm(self._end.timetuple()))
|
||||
store.startElement(tag, attrs)
|
||||
store.endElement(tag)
|
||||
|
||||
@property
|
||||
def creator(self):
|
||||
return self._creator
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self._start
|
||||
|
||||
@property
|
||||
def end(self):
|
||||
return self._end
|
||||
|
||||
@end.setter
|
||||
def end(self, c):
|
||||
self._end = c
|
||||
|
||||
@end.deleter
|
||||
def end(self):
|
||||
self._end = None
|
||||
|
||||
from more import Response
|
||||
|
||||
def help_full ():
|
||||
return "This module store a lot of events: ny, we, " + (", ".join(context.datas.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer"
|
||||
return "This module store a lot of events: ny, we, " + (", ".join(context.datas.keys()) if hasattr(context, "datas") else "") + "\n!eventslist: gets list of timer\n!start /something/: launch a timer"
|
||||
|
||||
|
||||
def load(context):
|
||||
#Define the index
|
||||
context.data.setIndex("name")
|
||||
context.set_knodes({
|
||||
"dict": DictNode,
|
||||
"event": Event,
|
||||
})
|
||||
|
||||
for evt in context.data.index.keys():
|
||||
if context.data.index[evt].hasAttribute("end"):
|
||||
event = ModuleEvent(call=fini, call_data=dict(strend=context.data.index[evt]))
|
||||
event._end = context.data.index[evt].getDate("end")
|
||||
idt = context.add_event(event)
|
||||
if idt is not None:
|
||||
context.data.index[evt]["_id"] = idt
|
||||
if context.data is None:
|
||||
context.set_default(DictNode())
|
||||
|
||||
# Relaunch all timers
|
||||
for kevt in context.data:
|
||||
if context.data[kevt].end:
|
||||
context.data[kevt]._evt = context.add_event(ModuleEvent(partial(fini, kevt, context.data[kevt]), offset=context.data[kevt].end - datetime.now(timezone.utc), interval=0))
|
||||
|
||||
|
||||
def fini(d, strend):
|
||||
context.send_response(strend["server"], Response("%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"]))
|
||||
context.data.delChild(context.data.index[strend["name"]])
|
||||
def fini(name, evt):
|
||||
context.send_response(evt._server, Response("%s arrivé à échéance." % name, channel=evt._channel, nick=evt.creator))
|
||||
evt._evt = None
|
||||
del context.data[name]
|
||||
context.save()
|
||||
|
||||
@hook("cmd_hook", "goûter")
|
||||
|
||||
@hook.command("goûter")
|
||||
def cmd_gouter(msg):
|
||||
ndate = datetime.now(timezone.utc)
|
||||
ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc)
|
||||
|
|
@ -52,7 +102,8 @@ def cmd_gouter(msg):
|
|||
"Nous avons %s de retard pour le goûter :("),
|
||||
channel=msg.channel)
|
||||
|
||||
@hook("cmd_hook", "week-end")
|
||||
|
||||
@hook.command("week-end")
|
||||
def cmd_we(msg):
|
||||
ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday())
|
||||
ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc)
|
||||
|
|
@ -61,23 +112,16 @@ def cmd_we(msg):
|
|||
"Youhou, on est en week-end depuis %s."),
|
||||
channel=msg.channel)
|
||||
|
||||
@hook("cmd_hook", "start")
|
||||
|
||||
@hook.command("start")
|
||||
def start_countdown(msg):
|
||||
"""!start /something/: launch a timer"""
|
||||
if len(msg.args) < 1:
|
||||
raise IRCException("indique le nom d'un événement à chronométrer")
|
||||
if msg.args[0] in context.data.index:
|
||||
raise IRCException("%s existe déjà." % msg.args[0])
|
||||
raise IMException("indique le nom d'un événement à chronométrer")
|
||||
if msg.args[0] in context.data:
|
||||
raise IMException("%s existe déjà." % msg.args[0])
|
||||
|
||||
strnd = ModuleState("strend")
|
||||
strnd["server"] = msg.server
|
||||
strnd["channel"] = msg.channel
|
||||
strnd["proprio"] = msg.nick
|
||||
strnd["start"] = msg.date
|
||||
strnd["name"] = msg.args[0]
|
||||
context.data.addChild(strnd)
|
||||
|
||||
evt = ModuleEvent(call=fini, call_data=dict(strend=strnd))
|
||||
evt = Event(server=msg.server, channel=msg.channel, creator=msg.frm, start_time=msg.date)
|
||||
|
||||
if len(msg.args) > 1:
|
||||
result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1])
|
||||
|
|
@ -95,156 +139,158 @@ def start_countdown(msg):
|
|||
if result2 is None or result2.group(4) is None: yea = now.year
|
||||
else: yea = int(result2.group(4))
|
||||
if result2 is not None and result3 is not None:
|
||||
strnd["end"] = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc)
|
||||
evt.end = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc)
|
||||
elif result2 is not None:
|
||||
strnd["end"] = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc)
|
||||
evt.end = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc)
|
||||
elif result3 is not None:
|
||||
if hou * 3600 + minu * 60 + sec > now.hour * 3600 + now.minute * 60 + now.second:
|
||||
strnd["end"] = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc)
|
||||
evt.end = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc)
|
||||
else:
|
||||
strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc)
|
||||
evt._end = strnd.getDate("end")
|
||||
strnd["_id"] = context.add_event(evt)
|
||||
evt.end = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc)
|
||||
except:
|
||||
context.data.delChild(strnd)
|
||||
raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
|
||||
raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
|
||||
|
||||
elif result1 is not None and len(result1) > 0:
|
||||
strnd["end"] = msg.date
|
||||
evt.end = msg.date
|
||||
for (t, g) in result1:
|
||||
if g is None or g == "" or g == "m" or g == "M":
|
||||
strnd["end"] += timedelta(minutes=int(t))
|
||||
evt.end += timedelta(minutes=int(t))
|
||||
elif g == "h" or g == "H":
|
||||
strnd["end"] += timedelta(hours=int(t))
|
||||
evt.end += timedelta(hours=int(t))
|
||||
elif g == "d" or g == "D" or g == "j" or g == "J":
|
||||
strnd["end"] += timedelta(days=int(t))
|
||||
evt.end += timedelta(days=int(t))
|
||||
elif g == "w" or g == "W":
|
||||
strnd["end"] += timedelta(days=int(t)*7)
|
||||
evt.end += timedelta(days=int(t)*7)
|
||||
elif g == "y" or g == "Y" or g == "a" or g == "A":
|
||||
strnd["end"] += timedelta(days=int(t)*365)
|
||||
evt.end += timedelta(days=int(t)*365)
|
||||
else:
|
||||
strnd["end"] += timedelta(seconds=int(t))
|
||||
evt._end = strnd.getDate("end")
|
||||
eid = context.add_event(evt)
|
||||
if eid is not None:
|
||||
strnd["_id"] = eid
|
||||
evt.end += timedelta(seconds=int(t))
|
||||
|
||||
else:
|
||||
raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
|
||||
|
||||
context.data[msg.args[0]] = evt
|
||||
context.save()
|
||||
if "end" in strnd:
|
||||
|
||||
if evt.end is not None:
|
||||
context.add_event(ModuleEvent(partial(fini, msg.args[0], evt),
|
||||
offset=evt.end - datetime.now(timezone.utc),
|
||||
interval=0))
|
||||
return Response("%s commencé le %s et se terminera le %s." %
|
||||
(msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S"),
|
||||
strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S")),
|
||||
nick=msg.frm)
|
||||
evt.end.strftime("%A %d %B %Y à %H:%M:%S")),
|
||||
channel=msg.channel)
|
||||
else:
|
||||
return Response("%s commencé le %s"% (msg.args[0],
|
||||
msg.date.strftime("%A %d %B %Y à %H:%M:%S")),
|
||||
nick=msg.frm)
|
||||
channel=msg.channel)
|
||||
|
||||
@hook("cmd_hook", "end")
|
||||
@hook("cmd_hook", "forceend")
|
||||
|
||||
@hook.command("end")
|
||||
@hook.command("forceend")
|
||||
def end_countdown(msg):
|
||||
if len(msg.args) < 1:
|
||||
raise IRCException("quel événement terminer ?")
|
||||
raise IMException("quel événement terminer ?")
|
||||
|
||||
if msg.args[0] in context.data.index:
|
||||
if context.data.index[msg.args[0]]["proprio"] == msg.nick or (msg.cmd == "forceend" and msg.frm_owner):
|
||||
duration = countdown(msg.date - context.data.index[msg.args[0]].getDate("start"))
|
||||
context.del_event(context.data.index[msg.args[0]]["_id"])
|
||||
context.data.delChild(context.data.index[msg.args[0]])
|
||||
if msg.args[0] in context.data:
|
||||
if context.data[msg.args[0]].creator == msg.frm or (msg.cmd == "forceend" and msg.frm_owner):
|
||||
duration = countdown(msg.date - context.data[msg.args[0]].start)
|
||||
del context.data[msg.args[0]]
|
||||
context.save()
|
||||
return Response("%s a duré %s." % (msg.args[0], duration),
|
||||
channel=msg.channel, nick=msg.nick)
|
||||
channel=msg.channel, nick=msg.frm)
|
||||
else:
|
||||
raise IRCException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data.index[msg.args[0]]["proprio"]))
|
||||
raise IMException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data[msg.args[0]].creator))
|
||||
else:
|
||||
return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.nick)
|
||||
return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.frm)
|
||||
|
||||
@hook("cmd_hook", "eventslist")
|
||||
|
||||
@hook.command("eventslist")
|
||||
def liste(msg):
|
||||
"""!eventslist: gets list of timer"""
|
||||
if len(msg.args):
|
||||
res = list()
|
||||
for user in msg.args:
|
||||
cmptr = [x["name"] for x in context.data.index.values() if x["proprio"] == user]
|
||||
if len(cmptr) > 0:
|
||||
res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr)))
|
||||
else:
|
||||
res.append("%s n'a pas créé de compteur" % user)
|
||||
return Response(" ; ".join(res), channel=msg.channel)
|
||||
else:
|
||||
return Response("Compteurs connus : %s." % ", ".join(context.data.index.keys()), channel=msg.channel)
|
||||
|
||||
@hook("cmd_default")
|
||||
def parseanswer(msg):
|
||||
if msg.cmd in context.data.index:
|
||||
res = Response(channel=msg.channel)
|
||||
|
||||
# Avoid message starting by ! which can be interpreted as command by other bots
|
||||
if msg.cmd[0] == "!":
|
||||
res.nick = msg.nick
|
||||
|
||||
if context.data.index[msg.cmd].name == "strend":
|
||||
if context.data.index[msg.cmd].hasAttribute("end"):
|
||||
res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")), countdown(context.data.index[msg.cmd].getDate("end") - msg.date)))
|
||||
for user in msg.args:
|
||||
cmptr = [k for k in context.data if context.data[k].creator == user]
|
||||
if len(cmptr) > 0:
|
||||
res.append_message(cmptr, title="Events created by %s" % user)
|
||||
else:
|
||||
res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start"))))
|
||||
else:
|
||||
res.append_message(countdown_format(context.data.index[msg.cmd].getDate("start"), context.data.index[msg.cmd]["msg_before"], context.data.index[msg.cmd]["msg_after"]))
|
||||
res.append_message("%s doesn't have any counting events" % user)
|
||||
return res
|
||||
else:
|
||||
return Response(list(context.data.keys()), channel=msg.channel, title="Known events")
|
||||
|
||||
|
||||
@hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data)
|
||||
def parseanswer(msg):
|
||||
res = Response(channel=msg.channel)
|
||||
|
||||
# Avoid message starting by ! which can be interpreted as command by other bots
|
||||
if msg.cmd[0] == "!":
|
||||
res.nick = msg.frm
|
||||
|
||||
if msg.cmd in context.data:
|
||||
if context.data[msg.cmd].end:
|
||||
res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start), countdown(context.data[msg.cmd].end - msg.date)))
|
||||
else:
|
||||
res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start)))
|
||||
else:
|
||||
res.append_message(countdown_format(context.data[msg.cmd].start, context.data[msg.cmd]["msg_before"], context.data[msg.cmd]["msg_after"]))
|
||||
return res
|
||||
|
||||
|
||||
RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I)
|
||||
|
||||
@hook("ask_default")
|
||||
@hook.ask(match=lambda msg: RGXP_ask.match(msg.message))
|
||||
def parseask(msg):
|
||||
if RGXP_ask.match(msg.text) is not None:
|
||||
name = re.match("^.*!([^ \"'@!]+).*$", msg.text)
|
||||
if name is None:
|
||||
raise IRCException("il faut que tu attribues une commande à l'événement.")
|
||||
if name.group(1) in context.data.index:
|
||||
raise IRCException("un événement portant ce nom existe déjà.")
|
||||
name = re.match("^.*!([^ \"'@!]+).*$", msg.message)
|
||||
if name is None:
|
||||
raise IMException("il faut que tu attribues une commande à l'événement.")
|
||||
if name.group(1) in context.data:
|
||||
raise IMException("un événement portant ce nom existe déjà.")
|
||||
|
||||
texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.text, re.I)
|
||||
if texts is not None and texts.group(3) is not None:
|
||||
extDate = extractDate(msg.text)
|
||||
if extDate is None or extDate == "":
|
||||
raise IRCException("la date de l'événement est invalide !")
|
||||
texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.message, re.I)
|
||||
if texts is not None and texts.group(3) is not None:
|
||||
extDate = extractDate(msg.message)
|
||||
if extDate is None or extDate == "":
|
||||
raise IMException("la date de l'événement est invalide !")
|
||||
|
||||
if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"):
|
||||
msg_after = texts.group (2)
|
||||
msg_before = texts.group (5)
|
||||
if (texts.group(4) is not None and (texts.group(4) == "après" or texts.group(4) == "apres" or texts.group(4) == "after")) or texts.group(1) is None:
|
||||
msg_before = texts.group (2)
|
||||
msg_after = texts.group (5)
|
||||
if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"):
|
||||
msg_after = texts.group(2)
|
||||
msg_before = texts.group(5)
|
||||
if (texts.group(4) is not None and (texts.group(4) == "après" or texts.group(4) == "apres" or texts.group(4) == "after")) or texts.group(1) is None:
|
||||
msg_before = texts.group(2)
|
||||
msg_after = texts.group(5)
|
||||
|
||||
if msg_before.find("%s") == -1 or msg_after.find("%s") == -1:
|
||||
raise IRCException("Pour que l'événement soit valide, ajouter %s à"
|
||||
" l'endroit où vous voulez que soit ajouté le"
|
||||
" compte à rebours.")
|
||||
if msg_before.find("%s") == -1 or msg_after.find("%s") == -1:
|
||||
raise IMException("Pour que l'événement soit valide, ajouter %s à"
|
||||
" l'endroit où vous voulez que soit ajouté le"
|
||||
" compte à rebours.")
|
||||
|
||||
evt = ModuleState("event")
|
||||
evt["server"] = msg.server
|
||||
evt["channel"] = msg.channel
|
||||
evt["proprio"] = msg.nick
|
||||
evt["name"] = name.group(1)
|
||||
evt["start"] = extDate
|
||||
evt["msg_after"] = msg_after
|
||||
evt["msg_before"] = msg_before
|
||||
context.data.addChild(evt)
|
||||
context.save()
|
||||
return Response("Nouvel événement !%s ajouté avec succès." % name.group(1),
|
||||
channel=msg.channel)
|
||||
evt = ModuleState("event")
|
||||
evt["server"] = msg.server
|
||||
evt["channel"] = msg.channel
|
||||
evt["proprio"] = msg.frm
|
||||
evt["name"] = name.group(1)
|
||||
evt["start"] = extDate
|
||||
evt["msg_after"] = msg_after
|
||||
evt["msg_before"] = msg_before
|
||||
context.data.addChild(evt)
|
||||
context.save()
|
||||
return Response("Nouvel événement !%s ajouté avec succès." % name.group(1),
|
||||
channel=msg.channel)
|
||||
|
||||
elif texts is not None and texts.group (2) is not None:
|
||||
evt = ModuleState("event")
|
||||
evt["server"] = msg.server
|
||||
evt["channel"] = msg.channel
|
||||
evt["proprio"] = msg.nick
|
||||
evt["name"] = name.group(1)
|
||||
evt["msg_before"] = texts.group (2)
|
||||
context.data.addChild(evt)
|
||||
context.save()
|
||||
return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1),
|
||||
channel=msg.channel)
|
||||
elif texts is not None and texts.group(2) is not None:
|
||||
evt = ModuleState("event")
|
||||
evt["server"] = msg.server
|
||||
evt["channel"] = msg.channel
|
||||
evt["proprio"] = msg.frm
|
||||
evt["name"] = name.group(1)
|
||||
evt["msg_before"] = texts.group (2)
|
||||
context.data.addChild(evt)
|
||||
context.save()
|
||||
return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1),
|
||||
channel=msg.channel)
|
||||
|
||||
else:
|
||||
raise IRCException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.")
|
||||
else:
|
||||
raise IMException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.")
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
"""URL reducer module"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.message import Text
|
||||
from nemubot.tools import web
|
||||
|
||||
|
||||
# MODULE FUCNTIONS ####################################################
|
||||
|
||||
def default_reducer(url, data):
|
||||
snd_url = url + quote(data, "/:%@&=?")
|
||||
return web.getURLContent(snd_url)
|
||||
|
||||
def framalink_reducer(url, data):
|
||||
json_data = json.loads(web.getURLContent(url, "lsturl="
|
||||
+ quote(data, "/:%@&=?"),
|
||||
header={"Content-Type": "application/x-www-form-urlencoded"}))
|
||||
return json_data['short']
|
||||
|
||||
# MODULE VARIABLES ####################################################
|
||||
|
||||
PROVIDERS = {
|
||||
"tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="),
|
||||
"ycc": (default_reducer, "http://ycc.fr/redirection/create/"),
|
||||
"framalink": (framalink_reducer, "https://frama.link/a?format=json")
|
||||
}
|
||||
DEFAULT_PROVIDER = "framalink"
|
||||
|
||||
PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, url in PROVIDERS.values()]
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
global DEFAULT_PROVIDER
|
||||
|
||||
if "provider" in context.config:
|
||||
if context.config["provider"] == "custom":
|
||||
PROVIDERS["custom"] = context.config["provider_url"]
|
||||
DEFAULT_PROVIDER = context.config["provider"]
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def reduce(url):
|
||||
"""Ask the url shortner website to reduce given URL
|
||||
|
||||
Argument:
|
||||
url -- the URL to reduce
|
||||
"""
|
||||
return PROVIDERS[DEFAULT_PROVIDER][0](PROVIDERS[DEFAULT_PROVIDER][1], url)
|
||||
|
||||
def gen_response(res, msg, srv):
|
||||
if res is None:
|
||||
raise IRCException("bad URL : %s" % srv)
|
||||
else:
|
||||
return Text("URL for %s: %s" % (srv, res), server=None,
|
||||
to=msg.to_response)
|
||||
|
||||
|
||||
## URL stack
|
||||
|
||||
LAST_URLS = dict()
|
||||
|
||||
|
||||
@hook("msg_default")
|
||||
def parselisten(msg):
|
||||
parseresponse(msg)
|
||||
return None
|
||||
|
||||
|
||||
@hook("all_post")
|
||||
def parseresponse(msg):
|
||||
global LAST_URLS
|
||||
if hasattr(msg, "text") and msg.text:
|
||||
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text)
|
||||
for url in urls:
|
||||
o = urlparse(web._getNormalizedURL(url), "http")
|
||||
|
||||
# Skip short URLs
|
||||
if o.netloc == "" or o.netloc in PROVIDERS or len(o.netloc) + len(o.path) < 17:
|
||||
continue
|
||||
|
||||
for recv in msg.receivers:
|
||||
if recv not in LAST_URLS:
|
||||
LAST_URLS[recv] = list()
|
||||
LAST_URLS[recv].append(url)
|
||||
return msg
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook("cmd_hook", "framalink",
|
||||
help="Reduce any given URL",
|
||||
help_usage={None: "Reduce the last URL said on the channel",
|
||||
"URL [URL ...]": "Reduce the given URL(s)"})
|
||||
def cmd_reduceurl(msg):
|
||||
minify = list()
|
||||
|
||||
if not len(msg.args):
|
||||
global LAST_URLS
|
||||
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
|
||||
minify.append(LAST_URLS[msg.channel].pop())
|
||||
else:
|
||||
raise IRCException("I have no more URL to reduce.")
|
||||
|
||||
if len(msg.args) > 4:
|
||||
raise IRCException("I cannot reduce as much URL at once.")
|
||||
else:
|
||||
minify += msg.args
|
||||
|
||||
res = list()
|
||||
for url in minify:
|
||||
o = urlparse(web.getNormalizedURL(url), "http")
|
||||
minief_url = reduce(url)
|
||||
if o.netloc == "":
|
||||
res.append(gen_response(minief_url, msg, o.scheme))
|
||||
else:
|
||||
res.append(gen_response(minief_url, msg, o.netloc))
|
||||
return res
|
||||
64
modules/freetarifs.py
Normal file
64
modules/freetarifs.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""Inform about Free Mobile tarifs"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import urllib.parse
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
ACT = {
|
||||
"ff_toFixe": "Appel vers les fixes",
|
||||
"ff_toMobile": "Appel vers les mobiles",
|
||||
"ff_smsSendedToCountry": "SMS vers le pays",
|
||||
"ff_mmsSendedToCountry": "MMS vers le pays",
|
||||
"fc_callToFrance": "Appel vers la France",
|
||||
"fc_smsToFrance": "SMS vers la france",
|
||||
"fc_mmsSended": "MMS vers la france",
|
||||
"fc_callToSameCountry": "Réception des appels",
|
||||
"fc_callReceived": "Appel dans le pays",
|
||||
"fc_smsReceived": "SMS (Réception)",
|
||||
"fc_mmsReceived": "MMS (Réception)",
|
||||
"fc_moDataFromCountry": "Data",
|
||||
}
|
||||
|
||||
def get_land_tarif(country, forfait="pkgFREE"):
|
||||
url = "http://mobile.international.free.fr/?" + urllib.parse.urlencode({'pays': country})
|
||||
page = web.getURLContent(url)
|
||||
soup = BeautifulSoup(page)
|
||||
|
||||
fact = soup.find(class_=forfait)
|
||||
|
||||
if fact is None:
|
||||
raise IMException("Country or forfait not found.")
|
||||
|
||||
res = {}
|
||||
for s in ACT.keys():
|
||||
try:
|
||||
res[s] = fact.find(attrs={"data-bind": "text: " + s}).text + " " + fact.find(attrs={"data-bind": "html: " + s + "Unit"}).text
|
||||
except AttributeError:
|
||||
res[s] = "inclus"
|
||||
|
||||
return res
|
||||
|
||||
@hook.command("freetarifs",
|
||||
help="Show Free Mobile tarifs for given contries",
|
||||
help_usage={"COUNTRY": "Show Free Mobile tarifs for given CONTRY"},
|
||||
keywords={
|
||||
"forfait=FORFAIT": "Related forfait between Free (default) and 2euro"
|
||||
})
|
||||
def get_freetarif(msg):
|
||||
res = Response(channel=msg.channel)
|
||||
|
||||
for country in msg.args:
|
||||
t = get_land_tarif(country.lower().capitalize(), "pkg" + (msg.kwargs["forfait"] if "forfait" in msg.kwargs else "FREE").upper())
|
||||
res.append_message(["\x02%s\x0F : %s" % (ACT[k], t[k]) for k in sorted(ACT.keys(), reverse=True)], title=country)
|
||||
|
||||
return res
|
||||
|
|
@ -1,40 +1,38 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Repositories, users or issues on GitHub"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
def help_full():
|
||||
return ("!github /repo/: Display information about /repo/.\n"
|
||||
"!github_user /user/: Display information about /user/.")
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def info_repos(repo):
|
||||
return web.getJSON("https://api.github.com/search/repositories?q=%s" %
|
||||
quote(repo), timeout=10)
|
||||
quote(repo))
|
||||
|
||||
|
||||
def info_user(username):
|
||||
user = web.getJSON("https://api.github.com/users/%s" % quote(username),
|
||||
timeout=10)
|
||||
user = web.getJSON("https://api.github.com/users/%s" % quote(username))
|
||||
|
||||
user["repos"] = web.getJSON("https://api.github.com/users/%s/"
|
||||
"repos?sort=updated" % quote(username),
|
||||
timeout=10)
|
||||
"repos?sort=updated" % quote(username))
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def user_keys(username):
|
||||
keys = web.getURLContent("https://github.com/%s.keys" % quote(username))
|
||||
return keys.split('\n')
|
||||
|
||||
|
||||
def info_issue(repo, issue=None):
|
||||
rp = info_repos(repo)
|
||||
if rp["items"]:
|
||||
|
|
@ -65,10 +63,16 @@ def info_commit(repo, commit=None):
|
|||
quote(fullname))
|
||||
|
||||
|
||||
@hook("cmd_hook", "github")
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("github",
|
||||
help="Display information about some repositories",
|
||||
help_usage={
|
||||
"REPO": "Display information about the repository REPO",
|
||||
})
|
||||
def cmd_github(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate a repository name to search")
|
||||
raise IMException("indicate a repository name to search")
|
||||
|
||||
repos = info_repos(" ".join(msg.args))
|
||||
|
||||
|
|
@ -93,10 +97,14 @@ def cmd_github(msg):
|
|||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "github_user")
|
||||
def cmd_github(msg):
|
||||
@hook.command("github_user",
|
||||
help="Display information about users",
|
||||
help_usage={
|
||||
"USERNAME": "Display information about the user USERNAME",
|
||||
})
|
||||
def cmd_github_user(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate a user name to search")
|
||||
raise IMException("indicate a user name to search")
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more user")
|
||||
|
||||
|
|
@ -121,15 +129,37 @@ def cmd_github(msg):
|
|||
user["html_url"],
|
||||
kf))
|
||||
else:
|
||||
raise IRCException("User not found")
|
||||
raise IMException("User not found")
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "github_issue")
|
||||
def cmd_github(msg):
|
||||
@hook.command("github_user_keys",
|
||||
help="Display user SSH keys",
|
||||
help_usage={
|
||||
"USERNAME": "Show USERNAME's SSH keys",
|
||||
})
|
||||
def cmd_github_user_keys(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate a repository to view its issues")
|
||||
raise IMException("indicate a user name to search")
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more keys")
|
||||
|
||||
for k in user_keys(" ".join(msg.args)):
|
||||
res.append_message(k)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@hook.command("github_issue",
|
||||
help="Display repository's issues",
|
||||
help_usage={
|
||||
"REPO": "Display latest issues created on REPO",
|
||||
"REPO #ISSUE": "Display the issue number #ISSUE for REPO",
|
||||
})
|
||||
def cmd_github_issue(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("indicate a repository to view its issues")
|
||||
|
||||
issue = None
|
||||
|
||||
|
|
@ -150,7 +180,7 @@ def cmd_github(msg):
|
|||
issues = info_issue(repo, issue)
|
||||
|
||||
if issues is None:
|
||||
raise IRCException("Repository not found")
|
||||
raise IMException("Repository not found")
|
||||
|
||||
for issue in issues:
|
||||
res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" %
|
||||
|
|
@ -164,10 +194,15 @@ def cmd_github(msg):
|
|||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "github_commit")
|
||||
def cmd_github(msg):
|
||||
@hook.command("github_commit",
|
||||
help="Display repository's commits",
|
||||
help_usage={
|
||||
"REPO": "Display latest commits on REPO",
|
||||
"REPO COMMIT": "Display details for the COMMIT on REPO",
|
||||
})
|
||||
def cmd_github_commit(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate a repository to view its commits")
|
||||
raise IMException("indicate a repository to view its commits")
|
||||
|
||||
commit = None
|
||||
if re.match("^[a-fA-F0-9]+$", msg.args[0]):
|
||||
|
|
@ -185,7 +220,7 @@ def cmd_github(msg):
|
|||
commits = info_commit(repo, commit)
|
||||
|
||||
if commits is None:
|
||||
raise IRCException("Repository not found")
|
||||
raise IMException("Repository or commit not found")
|
||||
|
||||
for commit in commits:
|
||||
res.append_message("Commit %s by %s on %s: %s" %
|
||||
|
|
|
|||
85
modules/grep.py
Normal file
85
modules/grep.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Filter messages, displaying lines matching a pattern"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.message import Command, Text
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def grep(fltr, cmd, msg, icase=False, only=False):
|
||||
"""Perform a grep like on known nemubot structures
|
||||
|
||||
Arguments:
|
||||
fltr -- The filter regexp
|
||||
cmd -- The subcommand to execute
|
||||
msg -- The original message
|
||||
icase -- like the --ignore-case parameter of grep
|
||||
only -- like the --only-matching parameter of grep
|
||||
"""
|
||||
|
||||
fltr = re.compile(fltr, re.I if icase else 0)
|
||||
|
||||
for r in context.subtreat(context.subparse(msg, cmd)):
|
||||
if isinstance(r, Response):
|
||||
for i in range(len(r.messages) - 1, -1, -1):
|
||||
if isinstance(r.messages[i], list):
|
||||
for j in range(len(r.messages[i]) - 1, -1, -1):
|
||||
res = fltr.match(r.messages[i][j])
|
||||
if not res:
|
||||
r.messages[i].pop(j)
|
||||
elif only:
|
||||
r.messages[i][j] = res.group(1) if fltr.groups else res.group(0)
|
||||
if len(r.messages[i]) <= 0:
|
||||
r.messages.pop(i)
|
||||
elif isinstance(r.messages[i], str):
|
||||
res = fltr.match(r.messages[i])
|
||||
if not res:
|
||||
r.messages.pop(i)
|
||||
elif only:
|
||||
r.messages[i] = res.group(1) if fltr.groups else res.group(0)
|
||||
yield r
|
||||
|
||||
elif isinstance(r, Text):
|
||||
res = fltr.match(r.message)
|
||||
if res:
|
||||
if only:
|
||||
r.message = res.group(1) if fltr.groups else res.group(0)
|
||||
yield r
|
||||
|
||||
else:
|
||||
yield r
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("grep",
|
||||
help="Display only lines from a subcommand matching the given pattern",
|
||||
help_usage={"PTRN !SUBCMD": "Filter SUBCMD command using the pattern PTRN"},
|
||||
keywords={
|
||||
"nocase": "Perform case-insensitive matching",
|
||||
"only": "Print only the matched parts of a matching line",
|
||||
})
|
||||
def cmd_grep(msg):
|
||||
if len(msg.args) < 2:
|
||||
raise IMException("Please provide a filter and a command")
|
||||
|
||||
only = "only" in msg.kwargs
|
||||
|
||||
l = [m for m in grep(msg.args[0] if len(msg.args[0]) and msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?",
|
||||
" ".join(msg.args[1:]),
|
||||
msg,
|
||||
icase="nocase" in msg.kwargs,
|
||||
only=only) if m is not None]
|
||||
|
||||
if len(l) <= 0:
|
||||
raise IMException("Pattern not found in output")
|
||||
|
||||
return l
|
||||
121
modules/imdb.py
121
modules/imdb.py
|
|
@ -1,112 +1,115 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Show many information about a movie or serie"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
def help_full():
|
||||
return "Search a movie title with: !imdbs <approximative title> ; View movie details with !imdb <title>"
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
|
||||
def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False):
|
||||
def get_movie_by_id(imdbid):
|
||||
"""Returns the information about the matching movie"""
|
||||
|
||||
# Built URL
|
||||
url = "http://www.omdbapi.com/?"
|
||||
if title is not None:
|
||||
url += "t=%s&" % urllib.parse.quote(title)
|
||||
if year is not None:
|
||||
url += "y=%s&" % urllib.parse.quote(year)
|
||||
if imdbid is not None:
|
||||
url += "i=%s&" % urllib.parse.quote(imdbid)
|
||||
if fullplot:
|
||||
url += "plot=full&"
|
||||
if tomatoes:
|
||||
url += "tomatoes=true&"
|
||||
url = "http://www.imdb.com/title/" + urllib.parse.quote(imdbid)
|
||||
soup = BeautifulSoup(web.getURLContent(url))
|
||||
|
||||
# Make the request
|
||||
data = web.getJSON(url)
|
||||
return {
|
||||
"imdbID": imdbid,
|
||||
"Title": soup.body.find('h1').contents[0].strip(),
|
||||
"Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("a")[1:]]),
|
||||
"Duration": soup.body.find(attrs={"class": "title_wrapper"}).find("time").text.strip() if soup.body.find(attrs={"class": "title_wrapper"}).find("time") else None,
|
||||
"imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip() if soup.body.find(attrs={"class": "ratingValue"}) else None,
|
||||
"imdbVotes": soup.body.find(attrs={"class": "imdbRating"}).find("a").text.strip() if soup.body.find(attrs={"class": "imdbRating"}) else None,
|
||||
"Plot": re.sub(r"\s+", " ", soup.body.find(attrs={"class": "summary_text"}).text).strip(),
|
||||
|
||||
# Return data
|
||||
if "Error" in data:
|
||||
raise IRCException(data["Error"])
|
||||
|
||||
elif "Response" in data and data["Response"] == "True":
|
||||
return data
|
||||
|
||||
else:
|
||||
raise IRCException("An error occurs during movie search")
|
||||
"Type": "TV Series" if soup.find(id="title-episode-widget") else "Movie",
|
||||
"Genre": ", ".join([x.text.strip() for x in soup.body.find(id="titleStoryLine").find_all("a") if x.get("href") is not None and x.get("href")[:21] == "/search/title?genres="]),
|
||||
"Country": ", ".join([x.text.strip() for x in soup.body.find(id="titleDetails").find_all("a") if x.get("href") is not None and x.get("href")[:32] == "/search/title?country_of_origin="]),
|
||||
"Credits": " ; ".join([x.find("h4").text.strip() + " " + (", ".join([y.text.strip() for y in x.find_all("a") if y.get("href") is not None and y.get("href")[:6] == "/name/"])) for x in soup.body.find_all(attrs={"class": "credit_summary_item"})]),
|
||||
}
|
||||
|
||||
|
||||
def find_movies(title):
|
||||
def find_movies(title, year=None):
|
||||
"""Find existing movies matching a approximate title"""
|
||||
|
||||
title = title.lower()
|
||||
|
||||
# Built URL
|
||||
url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(title)
|
||||
url = "https://v2.sg.media-imdb.com/suggests/%s/%s.json" % (urllib.parse.quote(title[0]), urllib.parse.quote(title.replace(" ", "_")))
|
||||
|
||||
# Make the request
|
||||
data = web.getJSON(url)
|
||||
|
||||
# Return data
|
||||
if "Error" in data:
|
||||
raise IRCException(data["Error"])
|
||||
|
||||
elif "Search" in data:
|
||||
return data
|
||||
data = web.getJSON(url, remove_callback=True)
|
||||
|
||||
if "d" not in data:
|
||||
return None
|
||||
elif year is None:
|
||||
return data["d"]
|
||||
else:
|
||||
raise IRCException("An error occurs during movie search")
|
||||
return [d for d in data["d"] if "y" in d and str(d["y"]) == year]
|
||||
|
||||
|
||||
@hook("cmd_hook", "imdb")
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("imdb",
|
||||
help="View movie/serie details, using OMDB",
|
||||
help_usage={
|
||||
"TITLE": "Look for a movie titled TITLE",
|
||||
"IMDB_ID": "Look for the movie with the given IMDB_ID",
|
||||
})
|
||||
def cmd_imdb(msg):
|
||||
"""View movie details with !imdb <title>"""
|
||||
if not len(msg.args):
|
||||
raise IRCException("precise a movie/serie title!")
|
||||
raise IMException("precise a movie/serie title!")
|
||||
|
||||
title = ' '.join(msg.args)
|
||||
|
||||
if re.match("^tt[0-9]{7}$", title) is not None:
|
||||
data = get_movie(imdbid=title)
|
||||
data = get_movie_by_id(imdbid=title)
|
||||
else:
|
||||
rm = re.match(r"^(.+)\s\(([0-9]{4})\)$", title)
|
||||
if rm is not None:
|
||||
data = get_movie(title=rm.group(1), year=rm.group(2))
|
||||
data = find_movies(rm.group(1), year=rm.group(2))
|
||||
else:
|
||||
data = get_movie(title=title)
|
||||
data = find_movies(title)
|
||||
|
||||
if not data:
|
||||
raise IMException("Movie/series not found")
|
||||
|
||||
data = get_movie_by_id(data[0]["id"])
|
||||
|
||||
res = Response(channel=msg.channel,
|
||||
title="%s (%s)" % (data['Title'], data['Year']),
|
||||
nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID'])
|
||||
|
||||
res.append_message("\x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" %
|
||||
(data['imdbRating'], data['imdbVotes'], data['Plot']))
|
||||
res.append_message("%s \x02genre:\x0F %s; \x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" %
|
||||
(data['Type'], data['Genre'], data['imdbRating'], data['imdbVotes'], data['Plot']))
|
||||
res.append_message("%s \x02from\x0F %s; %s"
|
||||
% (data['Type'], data['Country'], data['Credits']))
|
||||
|
||||
res.append_message("%s \x02from\x0F %s \x02released on\x0F %s; \x02genre:\x0F %s; \x02directed by:\x0F %s; \x02written by:\x0F %s; \x02main actors:\x0F %s"
|
||||
% (data['Type'], data['Country'], data['Released'], data['Genre'], data['Director'], data['Writer'], data['Actors']))
|
||||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "imdbs")
|
||||
@hook.command("imdbs",
|
||||
help="Search a movie/serie by title",
|
||||
help_usage={
|
||||
"TITLE": "Search a movie/serie by TITLE",
|
||||
})
|
||||
def cmd_search(msg):
|
||||
"""!imdbs <approximative title> to search a movie title"""
|
||||
if not len(msg.args):
|
||||
raise IRCException("precise a movie/serie title!")
|
||||
raise IMException("precise a movie/serie title!")
|
||||
|
||||
data = find_movies(' '.join(msg.args))
|
||||
|
||||
movies = list()
|
||||
for m in data['Search']:
|
||||
movies.append("\x02%s\x0F (%s of %s)" % (m['Title'], m['Type'], m['Year']))
|
||||
for m in data:
|
||||
movies.append("\x02%s\x0F%s with %s" % (m['l'], (" (" + str(m['y']) + ")") if "y" in m else "", m['s']))
|
||||
|
||||
return Response(movies, title="Titles found", channel=msg.channel)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
from bs4 import BeautifulSoup
|
||||
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.tools import web
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
import json
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
|
@ -41,18 +39,18 @@ def getJsonKeys(data):
|
|||
else:
|
||||
return data.keys()
|
||||
|
||||
@hook("cmd_hook", "json")
|
||||
@hook.command("json")
|
||||
def get_json_info(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Please specify a url and a list of JSON keys.")
|
||||
raise IMException("Please specify a url and a list of JSON keys.")
|
||||
|
||||
request_data = web.getURLContent(msg.args[0].replace(' ', "%20"))
|
||||
if not request_data:
|
||||
raise IRCException("Please specify a valid url.")
|
||||
raise IMException("Please specify a valid url.")
|
||||
json_data = json.loads(request_data)
|
||||
|
||||
if len(msg.args) == 1:
|
||||
raise IRCException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data)))
|
||||
raise IMException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data)))
|
||||
|
||||
tags = ','.join(msg.args[1:]).split(',')
|
||||
response = getRequestedTags(tags, json_data)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# coding=utf-8
|
||||
"""Read manual pages on IRC"""
|
||||
|
||||
"Read manual pages on IRC"
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
|
|
@ -8,18 +8,22 @@ import os
|
|||
|
||||
from nemubot.hooks import hook
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
def help_full():
|
||||
return "!man [0-9] /what/: gives informations about /what/."
|
||||
# GLOBALS #############################################################
|
||||
|
||||
RGXP_s = re.compile(b'\x1b\\[[0-9]+m')
|
||||
|
||||
|
||||
@hook("cmd_hook", "MAN")
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("MAN",
|
||||
help="Show man pages",
|
||||
help_usage={
|
||||
"SUBJECT": "Display the default man page for SUBJECT",
|
||||
"SECTION SUBJECT": "Display the man page in SECTION for SUBJECT"
|
||||
})
|
||||
def cmd_man(msg):
|
||||
args = ["man"]
|
||||
num = None
|
||||
|
|
@ -52,7 +56,11 @@ def cmd_man(msg):
|
|||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "man")
|
||||
@hook.command("man",
|
||||
help="Show man pages synopsis (in one line)",
|
||||
help_usage={
|
||||
"SUBJECT": "Display man page synopsis for SUBJECT",
|
||||
})
|
||||
def cmd_whatis(msg):
|
||||
args = ["whatis", " ".join(msg.args)]
|
||||
|
||||
|
|
@ -65,10 +73,6 @@ def cmd_whatis(msg):
|
|||
res.append_message(" ".join(line.decode().split()))
|
||||
|
||||
if len(res.messages) <= 0:
|
||||
if num is not None:
|
||||
res.append_message("There is no entry %s in section %d." %
|
||||
(msg.args[0], num))
|
||||
else:
|
||||
res.append_message("There is no man page for %s." % msg.args[0])
|
||||
res.append_message("There is no man page for %s." % msg.args[0])
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,33 +1,34 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Transform name location to GPS coordinates"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 4.0
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from more import Response
|
||||
# GLOBALS #############################################################
|
||||
|
||||
URL_API = "http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s"
|
||||
URL_API = "https://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s"
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
if not context.config or not context.config.hasAttribute("apikey"):
|
||||
if not context.config or "apikey" not in context.config:
|
||||
raise ImportError("You need a MapQuest API key in order to use this "
|
||||
"module. Add it to the module configuration file:\n"
|
||||
"<module name=\"mapquest\" key=\"XXXXXXXXXXXXXXXX\" "
|
||||
"/>\nRegister at http://developer.mapquest.com/")
|
||||
"/>\nRegister at https://developer.mapquest.com/")
|
||||
global URL_API
|
||||
URL_API = URL_API % context.config["apikey"].replace("%", "%%")
|
||||
|
||||
|
||||
def help_full():
|
||||
return "!geocode /place/: get coordinate of /place/."
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def geocode(location):
|
||||
obj = web.getJSON(URL_API % quote(location))
|
||||
|
|
@ -43,12 +44,18 @@ def where(loc):
|
|||
"{adminArea1}".format(**loc)).strip()
|
||||
|
||||
|
||||
@hook("cmd_hook", "geocode")
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("geocode",
|
||||
help="Get GPS coordinates of a place",
|
||||
help_usage={
|
||||
"PLACE": "Get GPS coordinates of PLACE"
|
||||
})
|
||||
def cmd_geocode(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate a name")
|
||||
raise IMException("indicate a name")
|
||||
|
||||
res = Response(channel=msg.channel, nick=msg.nick,
|
||||
res = Response(channel=msg.channel, nick=msg.frm,
|
||||
nomore="No more geocode", count=" (%s more geocode)")
|
||||
|
||||
for loc in geocode(' '.join(msg.args)):
|
||||
|
|
|
|||
|
|
@ -2,25 +2,24 @@
|
|||
|
||||
"""Use MediaWiki API to get pages"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# MEDIAWIKI REQUESTS ##################################################
|
||||
|
||||
def get_namespaces(site, ssl=False):
|
||||
def get_namespaces(site, ssl=False, path="/w/api.php"):
|
||||
# Built URL
|
||||
url = "http%s://%s/w/api.php?format=json&action=query&meta=siteinfo&siprop=namespaces" % (
|
||||
"s" if ssl else "", site)
|
||||
url = "http%s://%s%s?format=json&action=query&meta=siteinfo&siprop=namespaces" % (
|
||||
"s" if ssl else "", site, path)
|
||||
|
||||
# Make the request
|
||||
data = web.getJSON(url)
|
||||
|
|
@ -31,10 +30,10 @@ def get_namespaces(site, ssl=False):
|
|||
return namespaces
|
||||
|
||||
|
||||
def get_raw_page(site, term, ssl=False):
|
||||
def get_raw_page(site, term, ssl=False, path="/w/api.php"):
|
||||
# Built URL
|
||||
url = "http%s://%s/w/api.php?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % (
|
||||
"s" if ssl else "", site, urllib.parse.quote(term))
|
||||
url = "http%s://%s%s?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % (
|
||||
"s" if ssl else "", site, path, urllib.parse.quote(term))
|
||||
|
||||
# Make the request
|
||||
data = web.getJSON(url)
|
||||
|
|
@ -43,13 +42,13 @@ def get_raw_page(site, term, ssl=False):
|
|||
try:
|
||||
return data["query"]["pages"][k]["revisions"][0]["*"]
|
||||
except:
|
||||
raise IRCException("article not found")
|
||||
raise IMException("article not found")
|
||||
|
||||
|
||||
def get_unwikitextified(site, wikitext, ssl=False):
|
||||
def get_unwikitextified(site, wikitext, ssl=False, path="/w/api.php"):
|
||||
# Built URL
|
||||
url = "http%s://%s/w/api.php?format=json&action=expandtemplates&text=%s" % (
|
||||
"s" if ssl else "", site, urllib.parse.quote(wikitext))
|
||||
url = "http%s://%s%s?format=json&action=expandtemplates&text=%s" % (
|
||||
"s" if ssl else "", site, path, urllib.parse.quote(wikitext))
|
||||
|
||||
# Make the request
|
||||
data = web.getJSON(url)
|
||||
|
|
@ -59,25 +58,25 @@ def get_unwikitextified(site, wikitext, ssl=False):
|
|||
|
||||
## Search
|
||||
|
||||
def opensearch(site, term, ssl=False):
|
||||
def opensearch(site, term, ssl=False, path="/w/api.php"):
|
||||
# Built URL
|
||||
url = "http%s://%s/w/api.php?format=xml&action=opensearch&search=%s" % (
|
||||
"s" if ssl else "", site, urllib.parse.quote(term))
|
||||
url = "http%s://%s%s?format=json&action=opensearch&search=%s" % (
|
||||
"s" if ssl else "", site, path, urllib.parse.quote(term))
|
||||
|
||||
# Make the request
|
||||
response = web.getXML(url)
|
||||
response = web.getJSON(url)
|
||||
|
||||
if response is not None and response.hasNode("Section"):
|
||||
for itm in response.getNode("Section").getNodes("Item"):
|
||||
yield (itm.getNode("Text").getContent(),
|
||||
itm.getNode("Description").getContent() if itm.hasNode("Description") else "",
|
||||
itm.getNode("Url").getContent())
|
||||
if response is not None and len(response) >= 4:
|
||||
for k in range(len(response[1])):
|
||||
yield (response[1][k],
|
||||
response[2][k],
|
||||
response[3][k])
|
||||
|
||||
|
||||
def search(site, term, ssl=False):
|
||||
def search(site, term, ssl=False, path="/w/api.php"):
|
||||
# Built URL
|
||||
url = "http%s://%s/w/api.php?format=json&action=query&list=search&srsearch=%s&srprop=titlesnippet|snippet" % (
|
||||
"s" if ssl else "", site, urllib.parse.quote(term))
|
||||
url = "http%s://%s%s?format=json&action=query&list=search&srsearch=%s&srprop=titlesnippet|snippet" % (
|
||||
"s" if ssl else "", site, path, urllib.parse.quote(term))
|
||||
|
||||
# Make the request
|
||||
data = web.getJSON(url)
|
||||
|
|
@ -90,6 +89,11 @@ def search(site, term, ssl=False):
|
|||
|
||||
# PARSING FUNCTIONS ###################################################
|
||||
|
||||
def get_model(cnt, model="Infobox"):
|
||||
for full in re.findall(r"(\{\{" + model + " .*?(?:\{\{.*?}}.*?)*}})", cnt, flags=re.DOTALL):
|
||||
return full[3 + len(model):-2].replace("\n", " ").strip()
|
||||
|
||||
|
||||
def strip_model(cnt):
|
||||
# Strip models at begin: mostly useless
|
||||
cnt = re.sub(r"^(({{([^{]|\s|({{([^{]|\s|{{.*?}})*?}})*?)*?}}|\[\[([^[]|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL)
|
||||
|
|
@ -109,9 +113,9 @@ def strip_model(cnt):
|
|||
return cnt
|
||||
|
||||
|
||||
def parse_wikitext(site, cnt, namespaces=dict(), ssl=False):
|
||||
def parse_wikitext(site, cnt, namespaces=dict(), **kwargs):
|
||||
for i, _, _, _ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt):
|
||||
cnt = cnt.replace(i, get_unwikitextified(site, i, ssl), 1)
|
||||
cnt = cnt.replace(i, get_unwikitextified(site, i, **kwargs), 1)
|
||||
|
||||
# Strip [[...]]
|
||||
for full, args, lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt):
|
||||
|
|
@ -140,71 +144,106 @@ def irc_format(cnt):
|
|||
return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f")
|
||||
|
||||
|
||||
def get_page(site, term, ssl=False, subpart=None):
|
||||
raw = get_raw_page(site, term, ssl)
|
||||
def parse_infobox(cnt):
|
||||
for v in cnt.split("|"):
|
||||
try:
|
||||
yield re.sub(r"^\s*([^=]*[^=\s])\s*=\s*(.+)\s*$", "\x03\x02" + r"\1" + ":\x03\x02 " + r"\2", v).replace("<br />", ", ").replace("<br/>", ", ").strip()
|
||||
except:
|
||||
yield re.sub(r"^\s+(.+)\s+$", "\x03\x02" + r"\1" + "\x03\x02", v).replace("<br />", ", ").replace("<br/>", ", ").strip()
|
||||
|
||||
|
||||
def get_page(site, term, subpart=None, **kwargs):
|
||||
raw = get_raw_page(site, term, **kwargs)
|
||||
|
||||
if subpart is not None:
|
||||
subpart = subpart.replace("_", " ")
|
||||
raw = re.sub(r"^.*(?P<title>==+)\s*(" + subpart + r")\s*(?P=title)", r"\1 \2 \1", raw, flags=re.DOTALL)
|
||||
|
||||
return strip_model(raw)
|
||||
return raw
|
||||
|
||||
|
||||
# NEMUBOT #############################################################
|
||||
|
||||
def mediawiki_response(site, term, receivers):
|
||||
ns = get_namespaces(site)
|
||||
def mediawiki_response(site, term, to, **kwargs):
|
||||
ns = get_namespaces(site, **kwargs)
|
||||
|
||||
terms = term.split("#", 1)
|
||||
|
||||
try:
|
||||
# Print the article if it exists
|
||||
return Response(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None),
|
||||
line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)),
|
||||
channel=receivers)
|
||||
return Response(strip_model(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None, **kwargs)),
|
||||
line_treat=lambda line: irc_format(parse_wikitext(site, line, ns, **kwargs)),
|
||||
channel=to)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try looking at opensearch
|
||||
os = [x for x, _, _ in opensearch(site, terms[0])]
|
||||
os = [x for x, _, _ in opensearch(site, terms[0], **kwargs)]
|
||||
print(os)
|
||||
# Fallback to global search
|
||||
if not len(os):
|
||||
os = [x for x, _ in search(site, terms[0]) if x is not None and x != ""]
|
||||
os = [x for x, _ in search(site, terms[0], **kwargs) if x is not None and x != ""]
|
||||
return Response(os,
|
||||
channel=receivers,
|
||||
channel=to,
|
||||
title="Article not found, would you mean")
|
||||
|
||||
|
||||
@hook("cmd_hook", "mediawiki")
|
||||
@hook.command("mediawiki",
|
||||
help="Read an article on a MediaWiki",
|
||||
keywords={
|
||||
"ssl": "query over https instead of http",
|
||||
"path=PATH": "absolute path to the API",
|
||||
})
|
||||
def cmd_mediawiki(msg):
|
||||
"""Read an article on a MediaWiki"""
|
||||
if len(msg.args) < 2:
|
||||
raise IRCException("indicate a domain and a term to search")
|
||||
raise IMException("indicate a domain and a term to search")
|
||||
|
||||
return mediawiki_response(msg.args[0],
|
||||
" ".join(msg.args[1:]),
|
||||
msg.receivers)
|
||||
msg.to_response,
|
||||
**msg.kwargs)
|
||||
|
||||
|
||||
@hook("cmd_hook", "search_mediawiki")
|
||||
@hook.command("mediawiki_search",
|
||||
help="Search an article on a MediaWiki",
|
||||
keywords={
|
||||
"ssl": "query over https instead of http",
|
||||
"path=PATH": "absolute path to the API",
|
||||
})
|
||||
def cmd_srchmediawiki(msg):
|
||||
"""Search an article on a MediaWiki"""
|
||||
if len(msg.args) < 2:
|
||||
raise IRCException("indicate a domain and a term to search")
|
||||
raise IMException("indicate a domain and a term to search")
|
||||
|
||||
res = Response(channel=msg.receivers, nomore="No more results", count=" (%d more results)")
|
||||
res = Response(channel=msg.to_response, nomore="No more results", count=" (%d more results)")
|
||||
|
||||
for r in search(msg.args[0], " ".join(msg.args[1:])):
|
||||
for r in search(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs):
|
||||
res.append_message("%s: %s" % r)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "wikipedia")
|
||||
@hook.command("mediawiki_infobox",
|
||||
help="Highlight information from an article on a MediaWiki",
|
||||
keywords={
|
||||
"ssl": "query over https instead of http",
|
||||
"path=PATH": "absolute path to the API",
|
||||
})
|
||||
def cmd_infobox(msg):
|
||||
if len(msg.args) < 2:
|
||||
raise IMException("indicate a domain and a term to search")
|
||||
|
||||
ns = get_namespaces(msg.args[0], **msg.kwargs)
|
||||
|
||||
return Response(", ".join([x for x in parse_infobox(get_model(get_page(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs), "Infobox"))]),
|
||||
line_treat=lambda line: irc_format(parse_wikitext(msg.args[0], line, ns, **msg.kwargs)),
|
||||
channel=msg.to_response)
|
||||
|
||||
|
||||
@hook.command("wikipedia")
|
||||
def cmd_wikipedia(msg):
|
||||
if len(msg.args) < 2:
|
||||
raise IRCException("indicate a lang and a term to search")
|
||||
raise IMException("indicate a lang and a term to search")
|
||||
|
||||
return mediawiki_response(msg.args[0] + ".wikipedia.org",
|
||||
" ".join(msg.args[1:]),
|
||||
msg.receivers)
|
||||
msg.to_response)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from . import isup
|
||||
from . import page
|
||||
|
|
@ -38,28 +38,28 @@ def load(context):
|
|||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook("cmd_hook", "title",
|
||||
@hook.command("title",
|
||||
help="Retrieve webpage's title",
|
||||
help_usage={"URL": "Display the title of the given URL"})
|
||||
def cmd_title(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate the URL to visit.")
|
||||
raise IMException("Indicate the URL to visit.")
|
||||
|
||||
url = " ".join(msg.args)
|
||||
res = re.search("<title>(.*?)</title>", page.fetch(" ".join(msg.args)), re.DOTALL)
|
||||
|
||||
if res is None:
|
||||
raise IRCException("The page %s has no title" % url)
|
||||
raise IMException("The page %s has no title" % url)
|
||||
else:
|
||||
return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_hook", "curly",
|
||||
@hook.command("curly",
|
||||
help="Retrieve webpage's headers",
|
||||
help_usage={"URL": "Display HTTP headers of the given URL"})
|
||||
def cmd_curly(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate the URL to visit.")
|
||||
raise IMException("Indicate the URL to visit.")
|
||||
|
||||
url = " ".join(msg.args)
|
||||
version, status, reason, headers = page.headers(url)
|
||||
|
|
@ -67,12 +67,12 @@ def cmd_curly(msg):
|
|||
return Response("Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, version, status, reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in headers])), channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_hook", "curl",
|
||||
@hook.command("curl",
|
||||
help="Retrieve webpage's body",
|
||||
help_usage={"URL": "Display raw HTTP body of the given URL"})
|
||||
def cmd_curl(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate the URL to visit.")
|
||||
raise IMException("Indicate the URL to visit.")
|
||||
|
||||
res = Response(channel=msg.channel)
|
||||
for m in page.fetch(" ".join(msg.args)).split("\n"):
|
||||
|
|
@ -80,24 +80,24 @@ def cmd_curl(msg):
|
|||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "w3m",
|
||||
@hook.command("w3m",
|
||||
help="Retrieve and format webpage's content",
|
||||
help_usage={"URL": "Display and format HTTP content of the given URL"})
|
||||
def cmd_w3m(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate the URL to visit.")
|
||||
raise IMException("Indicate the URL to visit.")
|
||||
res = Response(channel=msg.channel)
|
||||
for line in page.render(" ".join(msg.args)).split("\n"):
|
||||
res.append_message(line)
|
||||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "traceurl",
|
||||
@hook.command("traceurl",
|
||||
help="Follow redirections of a given URL and display each step",
|
||||
help_usage={"URL": "Display redirections steps for the given URL"})
|
||||
def cmd_traceurl(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate an URL to trace!")
|
||||
raise IMException("Indicate an URL to trace!")
|
||||
|
||||
res = list()
|
||||
for url in msg.args[:4]:
|
||||
|
|
@ -109,12 +109,12 @@ def cmd_traceurl(msg):
|
|||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "isup",
|
||||
@hook.command("isup",
|
||||
help="Check if a website is up",
|
||||
help_usage={"DOMAIN": "Check if a DOMAIN is up"})
|
||||
def cmd_isup(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate an domain name to check!")
|
||||
raise IMException("Indicate an domain name to check!")
|
||||
|
||||
res = list()
|
||||
for url in msg.args[:4]:
|
||||
|
|
@ -126,12 +126,12 @@ def cmd_isup(msg):
|
|||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "w3c",
|
||||
@hook.command("w3c",
|
||||
help="Perform a w3c HTML validator check",
|
||||
help_usage={"URL": "Do W3C HTML validation on the given URL"})
|
||||
def cmd_w3c(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate an URL to validate!")
|
||||
raise IMException("Indicate an URL to validate!")
|
||||
|
||||
headers, validator = w3c.validator(msg.args[0])
|
||||
|
||||
|
|
@ -149,20 +149,20 @@ def cmd_w3c(msg):
|
|||
|
||||
|
||||
|
||||
@hook("cmd_hook", "watch", data="diff",
|
||||
@hook.command("watch", data="diff",
|
||||
help="Alert on webpage change",
|
||||
help_usage={"URL": "Watch the given URL and alert when it changes"})
|
||||
@hook("cmd_hook", "updown", data="updown",
|
||||
@hook.command("updown", data="updown",
|
||||
help="Alert on server availability change",
|
||||
help_usage={"URL": "Watch the given domain and alert when it availability status changes"})
|
||||
def cmd_watch(msg, diffType="diff"):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate an URL to watch!")
|
||||
raise IMException("indicate an URL to watch!")
|
||||
|
||||
return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType)
|
||||
|
||||
|
||||
@hook("cmd_hook", "listwatch",
|
||||
@hook.command("listwatch",
|
||||
help="List URL watched for the channel",
|
||||
help_usage={None: "List URL watched for the channel"})
|
||||
def cmd_listwatch(msg):
|
||||
|
|
@ -173,12 +173,12 @@ def cmd_listwatch(msg):
|
|||
return Response("No URL are currently watched. Use !watch URL to watch one.", channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_hook", "unwatch",
|
||||
@hook.command("unwatch",
|
||||
help="Unwatch a previously watched URL",
|
||||
help_usage={"URL": "Unwatch the given URL"})
|
||||
def cmd_unwatch(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("which URL should I stop watching?")
|
||||
raise IMException("which URL should I stop watching?")
|
||||
|
||||
for arg in msg.args:
|
||||
return watchWebsite.del_site(arg, msg.frm, msg.channel, msg.frm_owner)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def isup(url):
|
|||
|
||||
o = urllib.parse.urlparse(getNormalizedURL(url), "http")
|
||||
if o.netloc != "":
|
||||
isup = getJSON("http://isitup.org/%s.json" % o.netloc)
|
||||
isup = getJSON("https://isitup.org/%s.json" % o.netloc)
|
||||
if isup is not None and "status_code" in isup and isup["status_code"] == 1:
|
||||
return isup["response_time"]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import tempfile
|
|||
import urllib
|
||||
|
||||
from nemubot import __version__
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.tools import web
|
||||
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ def headers(url):
|
|||
|
||||
o = urllib.parse.urlparse(web.getNormalizedURL(url), "http")
|
||||
if o.netloc == "":
|
||||
raise IRCException("invalid URL")
|
||||
raise IMException("invalid URL")
|
||||
if o.scheme == "http":
|
||||
conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=5)
|
||||
else:
|
||||
|
|
@ -32,18 +32,18 @@ def headers(url):
|
|||
conn.request("HEAD", o.path, None, {"User-agent":
|
||||
"Nemubot v%s" % __version__})
|
||||
except ConnectionError as e:
|
||||
raise IRCException(e.strerror)
|
||||
raise IMException(e.strerror)
|
||||
except socket.timeout:
|
||||
raise IRCException("request timeout")
|
||||
raise IMException("request timeout")
|
||||
except socket.gaierror:
|
||||
print ("<tools.web> Unable to receive page %s from %s on %d."
|
||||
% (o.path, o.hostname, o.port if o.port is not None else 0))
|
||||
raise IRCException("an unexpected error occurs")
|
||||
raise IMException("an unexpected error occurs")
|
||||
|
||||
try:
|
||||
res = conn.getresponse()
|
||||
except http.client.BadStatusLine:
|
||||
raise IRCException("An error occurs")
|
||||
raise IMException("An error occurs")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ def headers(url):
|
|||
|
||||
|
||||
def _onNoneDefault():
|
||||
raise IRCException("An error occurs when trying to access the page")
|
||||
raise IMException("An error occurs when trying to access the page")
|
||||
|
||||
|
||||
def fetch(url, onNone=_onNoneDefault):
|
||||
|
|
@ -71,11 +71,11 @@ def fetch(url, onNone=_onNoneDefault):
|
|||
else:
|
||||
return None
|
||||
except ConnectionError as e:
|
||||
raise IRCException(e.strerror)
|
||||
raise IMException(e.strerror)
|
||||
except socket.timeout:
|
||||
raise IRCException("The request timeout when trying to access the page")
|
||||
raise IMException("The request timeout when trying to access the page")
|
||||
except socket.error as e:
|
||||
raise IRCException(e.strerror)
|
||||
raise IMException(e.strerror)
|
||||
|
||||
|
||||
def _render(cnt):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import json
|
|||
import urllib
|
||||
|
||||
from nemubot import __version__
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.tools.web import getNormalizedURL
|
||||
|
||||
def validator(url):
|
||||
|
|
@ -14,19 +14,19 @@ def validator(url):
|
|||
|
||||
o = urllib.parse.urlparse(getNormalizedURL(url), "http")
|
||||
if o.netloc == "":
|
||||
raise IRCException("Indicate a valid URL!")
|
||||
raise IMException("Indicate a valid URL!")
|
||||
|
||||
try:
|
||||
req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__})
|
||||
req = urllib.request.Request("https://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__})
|
||||
raw = urllib.request.urlopen(req, timeout=10)
|
||||
except urllib.error.HTTPError as e:
|
||||
raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason))
|
||||
raise IMException("HTTP error occurs: %s %s" % (e.code, e.reason))
|
||||
|
||||
headers = dict()
|
||||
for Hname, Hval in raw.getheaders():
|
||||
headers[Hname] = Hval
|
||||
|
||||
if "X-W3C-Validator-Status" not in headers or (headers["X-W3C-Validator-Status"] != "Valid" and headers["X-W3C-Validator-Status"] != "Invalid"):
|
||||
raise IRCException("Unexpected error on W3C servers" + (" (" + headers["X-W3C-Validator-Status"] + ")" if "X-W3C-Validator-Status" in headers else ""))
|
||||
raise IMException("Unexpected error on W3C servers" + (" (" + headers["X-W3C-Validator-Status"] + ")" if "X-W3C-Validator-Status" in headers else ""))
|
||||
|
||||
return headers, json.loads(raw.read().decode())
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
"""Alert on changes on websites"""
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from random import randint
|
||||
import urllib.parse
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from nemubot.event import ModuleEvent
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.tools.web import getNormalizedURL
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
logger = logging.getLogger("nemubot.module.networking.watchWebsite")
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from . import page
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ def del_site(url, nick, channel, frm_owner):
|
|||
for a in site.getNodes("alert"):
|
||||
if a["channel"] == channel:
|
||||
# if not (nick == a["nick"] or frm_owner):
|
||||
# raise IRCException("you cannot unwatch this URL.")
|
||||
# raise IMException("you cannot unwatch this URL.")
|
||||
site.delChild(a)
|
||||
if not site.hasNode("alert"):
|
||||
del_event(site["_evt_id"])
|
||||
|
|
@ -70,7 +70,7 @@ def del_site(url, nick, channel, frm_owner):
|
|||
save()
|
||||
return Response("I don't watch this URL anymore.",
|
||||
channel=channel, nick=nick)
|
||||
raise IRCException("I didn't watch this URL!")
|
||||
raise IMException("I didn't watch this URL!")
|
||||
|
||||
|
||||
def add_site(url, nick, channel, server, diffType="diff"):
|
||||
|
|
@ -82,7 +82,7 @@ def add_site(url, nick, channel, server, diffType="diff"):
|
|||
|
||||
o = urlparse(getNormalizedURL(url), "http")
|
||||
if o.netloc == "":
|
||||
raise IRCException("sorry, I can't watch this URL :(")
|
||||
raise IMException("sorry, I can't watch this URL :(")
|
||||
|
||||
alert = ModuleState("alert")
|
||||
alert["nick"] = nick
|
||||
|
|
@ -210,15 +210,14 @@ def start_watching(site, offset=0):
|
|||
offset -- offset time to delay the launch of the first check
|
||||
"""
|
||||
|
||||
o = urlparse(getNormalizedURL(site["url"]), "http")
|
||||
#print_debug("Add %s event for site: %s" % (site["type"], o.netloc))
|
||||
#o = urlparse(getNormalizedURL(site["url"]), "http")
|
||||
#print("Add %s event for site: %s" % (site["type"], o.netloc))
|
||||
|
||||
try:
|
||||
evt = ModuleEvent(func=fwatch,
|
||||
cmp_data=site["lastcontent"],
|
||||
func_data=site["url"], offset=offset,
|
||||
interval=site.getInt("time"),
|
||||
call=alert_change, call_data=site)
|
||||
evt = ModuleEvent(func=partial(fwatch, url=site["url"]),
|
||||
cmp=site["lastcontent"],
|
||||
offset=offset, interval=site.getInt("time"),
|
||||
call=partial(alert_change, site=site))
|
||||
site["_evt_id"] = add_event(evt)
|
||||
except IRCException:
|
||||
except IMException:
|
||||
logger.exception("Unable to watch %s", site["url"])
|
||||
|
|
|
|||
|
|
@ -1,61 +1,51 @@
|
|||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import datetime
|
||||
import urllib
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.tools.web import getJSON
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%%s&outputFormat=json&userName=%s&password=%s"
|
||||
URL_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s"
|
||||
URL_WHOIS = "https://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s"
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(CONF, add_hook):
|
||||
global URL_WHOIS
|
||||
global URL_AVAIL, URL_WHOIS
|
||||
|
||||
if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"):
|
||||
if not CONF or not CONF.hasNode("whoisxmlapi") or "username" not in CONF.getNode("whoisxmlapi") or "password" not in CONF.getNode("whoisxmlapi"):
|
||||
raise ImportError("You need a WhoisXML API account in order to use "
|
||||
"the !netwhois feature. Add it to the module "
|
||||
"configuration file:\n<whoisxmlapi username=\"XX\" "
|
||||
"password=\"XXX\" />\nRegister at "
|
||||
"http://www.whoisxmlapi.com/newaccount.php")
|
||||
"https://www.whoisxmlapi.com/newaccount.php")
|
||||
|
||||
URL_AVAIL = URL_AVAIL % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"]))
|
||||
URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"]))
|
||||
|
||||
import nemubot.hooks
|
||||
add_hook("cmd_hook", nemubot.hooks.Message(cmd_whois, "netwhois",
|
||||
help="Get whois information about given domains",
|
||||
help_usage={"DOMAIN": "Return whois information on the given DOMAIN"}))
|
||||
add_hook(nemubot.hooks.Command(cmd_whois, "netwhois",
|
||||
help="Get whois information about given domains",
|
||||
help_usage={"DOMAIN": "Return whois information on the given DOMAIN"}),
|
||||
"in","Command")
|
||||
add_hook(nemubot.hooks.Command(cmd_avail, "domain_available",
|
||||
help="Domain availability check using whoisxmlapi.com",
|
||||
help_usage={"DOMAIN": "Check if the given DOMAIN is available or not"}),
|
||||
"in","Command")
|
||||
|
||||
|
||||
def extractdate(str):
|
||||
tries = [
|
||||
"%Y-%m-%dT%H:%M:%S.0%Z",
|
||||
"%Y-%m-%dT%H:%M:%S%Z",
|
||||
"%Y-%m-%dT%H:%M:%S%z",
|
||||
"%Y-%m-%dT%H:%M:%SZ",
|
||||
"%Y-%m-%dT%H:%M:%S.0Z",
|
||||
"%Y-%m-%dT%H:%M:%S",
|
||||
"%Y-%m-%d %H:%M:%S.0%Z",
|
||||
"%Y-%m-%d %H:%M:%S%Z",
|
||||
"%Y-%m-%d %H:%M:%S%z",
|
||||
"%Y-%m-%d %H:%M:%S.0Z",
|
||||
"%Y-%m-%d %H:%M:%SZ",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%d",
|
||||
"%d/%m/%Y",
|
||||
]
|
||||
|
||||
for t in tries:
|
||||
try:
|
||||
return datetime.datetime.strptime(str, t)
|
||||
except ValueError:
|
||||
pass
|
||||
return datetime.datetime.strptime(str, t)
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def whois_entityformat(entity):
|
||||
ret = ""
|
||||
if "organization" in entity:
|
||||
ret += entity["organization"]
|
||||
if "organization" in entity and "name" in entity:
|
||||
ret += " "
|
||||
if "name" in entity:
|
||||
ret += entity["name"]
|
||||
|
||||
|
|
@ -77,30 +67,70 @@ def whois_entityformat(entity):
|
|||
|
||||
return ret.lstrip()
|
||||
|
||||
def available(dom):
|
||||
js = getJSON(URL_AVAIL % urllib.parse.quote(dom))
|
||||
|
||||
if "ErrorMessage" in js:
|
||||
raise IMException(js["ErrorMessage"]["msg"])
|
||||
|
||||
return js["DomainInfo"]["domainAvailability"] == "AVAILABLE"
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
def cmd_avail(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("Indicate a domain name for having its availability status!")
|
||||
|
||||
return Response(["%s: %s" % (dom, "available" if available(dom) else "unavailable") for dom in msg.args],
|
||||
channel=msg.channel)
|
||||
|
||||
|
||||
def cmd_whois(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indiquer un domaine ou une IP à whois !")
|
||||
raise IMException("Indiquer un domaine ou une IP à whois !")
|
||||
|
||||
dom = msg.args[0]
|
||||
|
||||
js = getJSON(URL_WHOIS % urllib.parse.quote(dom))
|
||||
|
||||
if "ErrorMessage" in js:
|
||||
err = js["ErrorMessage"]
|
||||
raise IRCException(js["ErrorMessage"]["msg"])
|
||||
raise IMException(js["ErrorMessage"]["msg"])
|
||||
|
||||
whois = js["WhoisRecord"]
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more whois information")
|
||||
res = []
|
||||
|
||||
res.append_message("%s: %s%s%s%s\x03\x02registered by\x03\x02 %s, \x03\x02administrated by\x03\x02 %s, \x03\x02managed by\x03\x02 %s" % (whois["domainName"],
|
||||
whois["status"].replace("\n", ", ") + " " if "status" in whois else "",
|
||||
"\x03\x02created on\x03\x02 " + extractdate(whois["createdDate"]).strftime("%c") + ", " if "createdDate" in whois else "",
|
||||
"\x03\x02updated on\x03\x02 " + extractdate(whois["updatedDate"]).strftime("%c") + ", " if "updatedDate" in whois else "",
|
||||
"\x03\x02expires on\x03\x02 " + extractdate(whois["expiresDate"]).strftime("%c") + ", " if "expiresDate" in whois else "",
|
||||
whois_entityformat(whois["registrant"]) if "registrant" in whois else "unknown",
|
||||
whois_entityformat(whois["administrativeContact"]) if "administrativeContact" in whois else "unknown",
|
||||
whois_entityformat(whois["technicalContact"]) if "technicalContact" in whois else "unknown",
|
||||
))
|
||||
return res
|
||||
if "registrarName" in whois:
|
||||
res.append("\x03\x02registered by\x03\x02 " + whois["registrarName"])
|
||||
|
||||
if "domainAvailability" in whois:
|
||||
res.append(whois["domainAvailability"])
|
||||
|
||||
if "contactEmail" in whois:
|
||||
res.append("\x03\x02contact email\x03\x02 " + whois["contactEmail"])
|
||||
|
||||
if "audit" in whois:
|
||||
if "createdDate" in whois["audit"] and "$" in whois["audit"]["createdDate"]:
|
||||
res.append("\x03\x02created on\x03\x02 " + whois["audit"]["createdDate"]["$"])
|
||||
if "updatedDate" in whois["audit"] and "$" in whois["audit"]["updatedDate"]:
|
||||
res.append("\x03\x02updated on\x03\x02 " + whois["audit"]["updatedDate"]["$"])
|
||||
|
||||
if "registryData" in whois:
|
||||
if "expiresDateNormalized" in whois["registryData"]:
|
||||
res.append("\x03\x02expire on\x03\x02 " + whois["registryData"]["expiresDateNormalized"])
|
||||
if "registrant" in whois["registryData"]:
|
||||
res.append("\x03\x02registrant:\x03\x02 " + whois_entityformat(whois["registryData"]["registrant"]))
|
||||
if "zoneContact" in whois["registryData"]:
|
||||
res.append("\x03\x02zone contact:\x03\x02 " + whois_entityformat(whois["registryData"]["zoneContact"]))
|
||||
if "technicalContact" in whois["registryData"]:
|
||||
res.append("\x03\x02technical contact:\x03\x02 " + whois_entityformat(whois["registryData"]["technicalContact"]))
|
||||
if "administrativeContact" in whois["registryData"]:
|
||||
res.append("\x03\x02administrative contact:\x03\x02 " + whois_entityformat(whois["registryData"]["administrativeContact"]))
|
||||
if "billingContact" in whois["registryData"]:
|
||||
res.append("\x03\x02billing contact:\x03\x02 " + whois_entityformat(whois["registryData"]["billingContact"]))
|
||||
|
||||
return Response(res,
|
||||
title=whois["domainName"],
|
||||
channel=msg.channel,
|
||||
nomore="No more whois information")
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ from urllib.parse import urljoin
|
|||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
from nemubot.module.urlreducer import reduce_inline
|
||||
from nemubot.tools.feed import Feed, AtomEntry
|
||||
|
||||
|
||||
|
|
@ -41,19 +42,20 @@ def get_last_news(url):
|
|||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook("cmd_hook", "news")
|
||||
@hook.command("news")
|
||||
def cmd_news(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate the URL to visit.")
|
||||
raise IMException("Indicate the URL to visit.")
|
||||
|
||||
url = " ".join(msg.args)
|
||||
links = [x for x in find_rss_links(url)]
|
||||
if len(links) == 0: links = [ url ]
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more news from %s" % url)
|
||||
res = Response(channel=msg.channel, nomore="No more news from %s" % url, line_treat=reduce_inline)
|
||||
for n in get_last_news(links[0]):
|
||||
res.append_message("%s published %s: %s %s" % (("\x02" + web.striphtml(n.title) + "\x0F") if n.title else "An article without title",
|
||||
(n.updated.strftime("on %A %d. %B %Y at %H:%M") if n.updated else "someday") if isinstance(n, AtomEntry) else n.pubDate,
|
||||
web.striphtml(n.summary) if n.summary else "",
|
||||
n.link if n.link else ""))
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<nemubotmodule name="nextstop">
|
||||
<message type="cmd" name="ratp" call="ask_ratp" />
|
||||
</nemubotmodule>
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Informe les usagers des prochains passages des transports en communs de la RATP"""
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.hooks import hook
|
||||
from more import Response
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from .external.src import ratp
|
||||
|
||||
def help_full ():
|
||||
return "!ratp transport line [station]: Donne des informations sur les prochains passages du transport en commun séléctionné à l'arrêt désiré. Si aucune station n'est précisée, les liste toutes."
|
||||
|
||||
|
||||
@hook("cmd_hook", "ratp")
|
||||
def ask_ratp(msg):
|
||||
"""Hook entry from !ratp"""
|
||||
if len(msg.args) >= 3:
|
||||
transport = msg.args[0]
|
||||
line = msg.args[1]
|
||||
station = msg.args[2]
|
||||
if len(msg.args) == 4:
|
||||
times = ratp.getNextStopsAtStation(transport, line, station, msg.args[3])
|
||||
else:
|
||||
times = ratp.getNextStopsAtStation(transport, line, station)
|
||||
|
||||
if len(times) == 0:
|
||||
raise IRCException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line))
|
||||
|
||||
(time, direction, stationname) = times[0]
|
||||
return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times],
|
||||
title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname),
|
||||
channel=msg.channel)
|
||||
|
||||
elif len(msg.args) == 2:
|
||||
stations = ratp.getAllStations(msg.args[0], msg.args[1])
|
||||
|
||||
if len(stations) == 0:
|
||||
raise IRCException("aucune station trouvée.")
|
||||
return Response([s for s in stations], title="Stations", channel=msg.channel)
|
||||
|
||||
else:
|
||||
raise IRCException("Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.")
|
||||
|
||||
@hook("cmd_hook", "ratp_alert")
|
||||
def ratp_alert(msg):
|
||||
if len(msg.args) == 2:
|
||||
transport = msg.args[0]
|
||||
cause = msg.args[1]
|
||||
incidents = ratp.getDisturbance(cause, transport)
|
||||
return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)")
|
||||
else:
|
||||
raise IRCException("Mauvais usage, merci de spécifier un type de transport et un type d'alerte (alerte, manif, travaux), ou de consulter l'aide du module.")
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 3d5c9b2d52fbd214f5aaad00e5f3952de919b3e5
|
||||
229
modules/nntp.py
Normal file
229
modules/nntp.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
"""The NNTP module"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import email
|
||||
import email.policy
|
||||
from email.utils import mktime_tz, parseaddr, parsedate_tz
|
||||
from functools import partial
|
||||
from nntplib import NNTP, decode_header
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from zlib import adler32
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.event import ModuleEvent
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
for wn in context.data.getNodes("watched_newsgroup"):
|
||||
watch(**wn.attributes)
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def list_groups(group_pattern="*", **server):
|
||||
with NNTP(**server) as srv:
|
||||
response, l = srv.list(group_pattern)
|
||||
for i in l:
|
||||
yield i.group, srv.description(i.group), i.flag
|
||||
|
||||
def read_group(group, **server):
|
||||
with NNTP(**server) as srv:
|
||||
response, count, first, last, name = srv.group(group)
|
||||
resp, overviews = srv.over((first, last))
|
||||
for art_num, over in reversed(overviews):
|
||||
yield over
|
||||
|
||||
def read_article(msg_id, **server):
|
||||
with NNTP(**server) as srv:
|
||||
response, info = srv.article(msg_id)
|
||||
return email.message_from_bytes(b"\r\n".join(info.lines), policy=email.policy.SMTPUTF8)
|
||||
|
||||
|
||||
servers_lastcheck = dict()
|
||||
servers_lastseen = dict()
|
||||
|
||||
def whatsnew(group="*", **server):
|
||||
fill = dict()
|
||||
if "user" in server: fill["user"] = server["user"]
|
||||
if "password" in server: fill["password"] = server["password"]
|
||||
if "host" in server: fill["host"] = server["host"]
|
||||
if "port" in server: fill["port"] = server["port"]
|
||||
|
||||
idx = _indexServer(**server)
|
||||
if idx in servers_lastcheck and servers_lastcheck[idx] is not None:
|
||||
date_last_check = servers_lastcheck[idx]
|
||||
else:
|
||||
date_last_check = datetime.now()
|
||||
|
||||
if idx not in servers_lastseen:
|
||||
servers_lastseen[idx] = []
|
||||
|
||||
with NNTP(**fill) as srv:
|
||||
response, servers_lastcheck[idx] = srv.date()
|
||||
|
||||
response, groups = srv.newgroups(date_last_check)
|
||||
for g in groups:
|
||||
yield g
|
||||
|
||||
response, articles = srv.newnews(group, date_last_check)
|
||||
for msg_id in articles:
|
||||
if msg_id not in servers_lastseen[idx]:
|
||||
servers_lastseen[idx].append(msg_id)
|
||||
response, info = srv.article(msg_id)
|
||||
yield email.message_from_bytes(b"\r\n".join(info.lines))
|
||||
|
||||
# Clean huge lists
|
||||
if len(servers_lastseen[idx]) > 42:
|
||||
servers_lastseen[idx] = servers_lastseen[idx][23:]
|
||||
|
||||
|
||||
def format_article(art, **response_args):
|
||||
art["X-FromName"], art["X-FromEmail"] = parseaddr(art["From"] if "From" in art else "")
|
||||
if art["X-FromName"] == '': art["X-FromName"] = art["X-FromEmail"]
|
||||
|
||||
date = mktime_tz(parsedate_tz(art["Date"]))
|
||||
if date < time.time() - 120:
|
||||
title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: on \x0F{Date}\x0314 by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F"
|
||||
else:
|
||||
title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F"
|
||||
|
||||
return Response(art.get_payload().replace('\n', ' '),
|
||||
title=title.format(adler32(art["Newsgroups"].encode()) & 0xf, adler32(art["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in art.items()}),
|
||||
**response_args)
|
||||
|
||||
|
||||
watches = dict()
|
||||
|
||||
def _indexServer(**kwargs):
|
||||
if "user" not in kwargs: kwargs["user"] = ""
|
||||
if "password" not in kwargs: kwargs["password"] = ""
|
||||
if "host" not in kwargs: kwargs["host"] = ""
|
||||
if "port" not in kwargs: kwargs["port"] = 119
|
||||
return "{user}:{password}@{host}:{port}".format(**kwargs)
|
||||
|
||||
def _newevt(**args):
|
||||
context.add_event(ModuleEvent(call=partial(_ticker, **args), interval=42))
|
||||
|
||||
def _ticker(to_server, to_channel, group, server):
|
||||
_newevt(to_server=to_server, to_channel=to_channel, group=group, server=server)
|
||||
n = 0
|
||||
for art in whatsnew(group, **server):
|
||||
n += 1
|
||||
if n > 10:
|
||||
continue
|
||||
context.send_response(to_server, format_article(art, channel=to_channel))
|
||||
if n > 10:
|
||||
context.send_response(to_server, Response("... and %s others news" % (n - 10), channel=to_channel))
|
||||
|
||||
def watch(to_server, to_channel, group="*", **server):
|
||||
_newevt(to_server=to_server, to_channel=to_channel, group=group, server=server)
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
keywords_server = {
|
||||
"host=HOST": "hostname or IP of the NNTP server",
|
||||
"port=PORT": "port of the NNTP server",
|
||||
"user=USERNAME": "username to use to connect to the server",
|
||||
"password=PASSWORD": "password to use to connect to the server",
|
||||
}
|
||||
|
||||
@hook.command("nntp_groups",
|
||||
help="Show list of existing groups",
|
||||
help_usage={
|
||||
None: "Display all groups",
|
||||
"PATTERN": "Filter on group matching the PATTERN"
|
||||
},
|
||||
keywords=keywords_server)
|
||||
def cmd_groups(msg):
|
||||
if "host" not in msg.kwargs:
|
||||
raise IMException("please give a hostname in keywords")
|
||||
|
||||
return Response(["\x02\x03{0:02d}{1}\x0F: {2}".format(adler32(g[0].encode()) & 0xf, *g) for g in list_groups(msg.args[0] if len(msg.args) > 0 else "*", **msg.kwargs)],
|
||||
channel=msg.channel,
|
||||
title="Matching groups on %s" % msg.kwargs["host"])
|
||||
|
||||
|
||||
@hook.command("nntp_overview",
|
||||
help="Show an overview of articles in given group(s)",
|
||||
help_usage={
|
||||
"GROUP": "Filter on group matching the PATTERN"
|
||||
},
|
||||
keywords=keywords_server)
|
||||
def cmd_overview(msg):
|
||||
if "host" not in msg.kwargs:
|
||||
raise IMException("please give a hostname in keywords")
|
||||
|
||||
if not len(msg.args):
|
||||
raise IMException("which group would you overview?")
|
||||
|
||||
for g in msg.args:
|
||||
arts = []
|
||||
for grp in read_group(g, **msg.kwargs):
|
||||
grp["X-FromName"], grp["X-FromEmail"] = parseaddr(grp["from"] if "from" in grp else "")
|
||||
if grp["X-FromName"] == '': grp["X-FromName"] = grp["X-FromEmail"]
|
||||
|
||||
arts.append("On {date}, from \x03{0:02d}{X-FromName}\x0F \x02{subject}\x0F: \x0314{message-id}\x0F".format(adler32(grp["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in grp.items()}))
|
||||
|
||||
if len(arts):
|
||||
yield Response(arts,
|
||||
channel=msg.channel,
|
||||
title="In \x03{0:02d}{1}\x0F".format(adler32(g[0].encode()) & 0xf, g))
|
||||
|
||||
|
||||
@hook.command("nntp_read",
|
||||
help="Read an article from a server",
|
||||
help_usage={
|
||||
"MSG_ID": "Read the given message"
|
||||
},
|
||||
keywords=keywords_server)
|
||||
def cmd_read(msg):
|
||||
if "host" not in msg.kwargs:
|
||||
raise IMException("please give a hostname in keywords")
|
||||
|
||||
for msgid in msg.args:
|
||||
if not re.match("<.*>", msgid):
|
||||
msgid = "<" + msgid + ">"
|
||||
art = read_article(msgid, **msg.kwargs)
|
||||
yield format_article(art, channel=msg.channel)
|
||||
|
||||
|
||||
@hook.command("nntp_watch",
|
||||
help="Launch an event looking for new groups and articles on a server",
|
||||
help_usage={
|
||||
None: "Watch all groups",
|
||||
"PATTERN": "Limit the watch on group matching this PATTERN"
|
||||
},
|
||||
keywords=keywords_server)
|
||||
def cmd_watch(msg):
|
||||
if "host" not in msg.kwargs:
|
||||
raise IMException("please give a hostname in keywords")
|
||||
|
||||
if not msg.frm_owner:
|
||||
raise IMException("sorry, this command is currently limited to the owner")
|
||||
|
||||
wnnode = ModuleState("watched_newsgroup")
|
||||
wnnode["id"] = _indexServer(**msg.kwargs)
|
||||
wnnode["to_server"] = msg.server
|
||||
wnnode["to_channel"] = msg.channel
|
||||
wnnode["group"] = msg.args[0] if len(msg.args) > 0 else "*"
|
||||
|
||||
wnnode["user"] = msg.kwargs["user"] if "user" in msg.kwargs else ""
|
||||
wnnode["password"] = msg.kwargs["password"] if "password" in msg.kwargs else ""
|
||||
wnnode["host"] = msg.kwargs["host"] if "host" in msg.kwargs else ""
|
||||
wnnode["port"] = msg.kwargs["port"] if "port" in msg.kwargs else 119
|
||||
|
||||
context.data.addChild(wnnode)
|
||||
watch(**wnnode.attributes)
|
||||
|
||||
return Response("Ok ok, I watch this newsgroup!", channel=msg.channel)
|
||||
87
modules/openai.py
Normal file
87
modules/openai.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Perform requests to openai"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
CLIENT = None
|
||||
MODEL = "gpt-4"
|
||||
ENDPOINT = None
|
||||
|
||||
def load(context):
|
||||
global CLIENT, ENDPOINT, MODEL
|
||||
if not context.config or ("apikey" not in context.config and "endpoint" not in context.config):
|
||||
raise ImportError ("You need a OpenAI API key in order to use "
|
||||
"this module. Add it to the module configuration: "
|
||||
"\n<module name=\"openai\" "
|
||||
"apikey=\"XXXXXX-XXXXXXXXXX\" endpoint=\"https://...\" model=\"gpt-4\" />")
|
||||
kwargs = {
|
||||
"api_key": context.config["apikey"] or "",
|
||||
}
|
||||
|
||||
if "endpoint" in context.config:
|
||||
ENDPOINT = context.config["endpoint"]
|
||||
kwargs["base_url"] = ENDPOINT
|
||||
|
||||
CLIENT = OpenAI(**kwargs)
|
||||
|
||||
if "model" in context.config:
|
||||
MODEL = context.config["model"]
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("list_models",
|
||||
help="list available LLM")
|
||||
def cmd_listllm(msg):
|
||||
llms = web.getJSON(ENDPOINT + "/models", timeout=6)
|
||||
return Response(message=[m for m in map(lambda i: i["id"], llms["data"])], title="Here is the available models", channel=msg.channel)
|
||||
|
||||
|
||||
@hook.command("set_model",
|
||||
help="Set the model to use when talking to nemubot")
|
||||
def cmd_setllm(msg):
|
||||
if len(msg.args) != 1:
|
||||
raise IMException("Indicate 1 model to use")
|
||||
|
||||
wanted_model = msg.args[0]
|
||||
|
||||
llms = web.getJSON(ENDPOINT + "/models", timeout=6)
|
||||
for model in llms["data"]:
|
||||
if wanted_model == model["id"]:
|
||||
break
|
||||
else:
|
||||
raise IMException("Unable to set such model: unknown")
|
||||
|
||||
MODEL = wanted_model
|
||||
return Response("New model in use: " + wanted_model, channel=msg.channel)
|
||||
|
||||
|
||||
@hook.ask()
|
||||
def parseask(msg):
|
||||
chat_completion = CLIENT.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a kind multilingual assistant. Respond to the user request in 255 characters maximum. Be conscise, go directly to the point. Never add useless terms.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": msg.message,
|
||||
}
|
||||
],
|
||||
model=MODEL,
|
||||
)
|
||||
|
||||
return Response(chat_completion.choices[0].message.content,
|
||||
msg.channel,
|
||||
msg.frm)
|
||||
158
modules/openroute.py
Normal file
158
modules/openroute.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""Lost? use our commands to find your way!"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
URL_DIRECTIONS_API = "https://api.openrouteservice.org/directions?api_key=%s&"
|
||||
URL_GEOCODE_API = "https://api.openrouteservice.org/geocoding?api_key=%s&"
|
||||
|
||||
waytype = [
|
||||
"unknown",
|
||||
"state road",
|
||||
"road",
|
||||
"street",
|
||||
"path",
|
||||
"track",
|
||||
"cycleway",
|
||||
"footway",
|
||||
"steps",
|
||||
"ferry",
|
||||
"construction",
|
||||
]
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
if not context.config or "apikey" not in context.config:
|
||||
raise ImportError("You need an OpenRouteService API key in order to use this "
|
||||
"module. Add it to the module configuration file:\n"
|
||||
"<module name=\"ors\" apikey=\"XXXXXXXXXXXXXXXX\" "
|
||||
"/>\nRegister at https://developers.openrouteservice.org")
|
||||
global URL_DIRECTIONS_API
|
||||
URL_DIRECTIONS_API = URL_DIRECTIONS_API % context.config["apikey"]
|
||||
global URL_GEOCODE_API
|
||||
URL_GEOCODE_API = URL_GEOCODE_API % context.config["apikey"]
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def approx_distance(lng):
|
||||
if lng > 1111:
|
||||
return "%f km" % (lng / 1000)
|
||||
else:
|
||||
return "%f m" % lng
|
||||
|
||||
|
||||
def approx_duration(sec):
|
||||
days = int(sec / 86400)
|
||||
if days > 0:
|
||||
return "%d days %f hours" % (days, (sec % 86400) / 3600)
|
||||
hours = int((sec % 86400) / 3600)
|
||||
if hours > 0:
|
||||
return "%d hours %f minutes" % (hours, (sec % 3600) / 60)
|
||||
minutes = (sec % 3600) / 60
|
||||
if minutes > 0:
|
||||
return "%d minutes" % minutes
|
||||
else:
|
||||
return "%d seconds" % sec
|
||||
|
||||
|
||||
def geocode(query, limit=7):
|
||||
obj = web.getJSON(URL_GEOCODE_API + urllib.parse.urlencode({
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
}))
|
||||
|
||||
for f in obj["features"]:
|
||||
yield f["geometry"]["coordinates"], f["properties"]
|
||||
|
||||
|
||||
def firstgeocode(query):
|
||||
for g in geocode(query, limit=1):
|
||||
return g
|
||||
|
||||
|
||||
def where(loc):
|
||||
return "{name} {city} {state} {county} {country}".format(**loc)
|
||||
|
||||
|
||||
def directions(coordinates, **kwargs):
|
||||
kwargs['coordinates'] = '|'.join(coordinates)
|
||||
|
||||
print(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs))
|
||||
return web.getJSON(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs), decode_error=True)
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("geocode",
|
||||
help="Get GPS coordinates of a place",
|
||||
help_usage={
|
||||
"PLACE": "Get GPS coordinates of PLACE"
|
||||
})
|
||||
def cmd_geocode(msg):
|
||||
res = Response(channel=msg.channel, nick=msg.frm,
|
||||
nomore="No more geocode", count=" (%s more geocode)")
|
||||
|
||||
for loc in geocode(' '.join(msg.args)):
|
||||
res.append_message("%s is at %s,%s" % (
|
||||
where(loc[1]),
|
||||
loc[0][1], loc[0][0],
|
||||
))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@hook.command("directions",
|
||||
help="Get routing instructions",
|
||||
help_usage={
|
||||
"POINT1 POINT2 ...": "Get routing instructions to go from POINT1 to the last POINTX via intermediates POINTX"
|
||||
},
|
||||
keywords={
|
||||
"profile=PROF": "One of driving-car, driving-hgv, cycling-regular, cycling-road, cycling-safe, cycling-mountain, cycling-tour, cycling-electric, foot-walking, foot-hiking, wheelchair. Default: foot-walking",
|
||||
"preference=PREF": "One of fastest, shortest, recommended. Default: recommended",
|
||||
"lang=LANG": "default: en",
|
||||
})
|
||||
def cmd_directions(msg):
|
||||
drcts = directions(["{0},{1}".format(*firstgeocode(g)[0]) for g in msg.args],
|
||||
profile=msg.kwargs["profile"] if "profile" in msg.kwargs else "foot-walking",
|
||||
preference=msg.kwargs["preference"] if "preference" in msg.kwargs else "recommended",
|
||||
units="m",
|
||||
language=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
|
||||
geometry=False,
|
||||
instructions=True,
|
||||
instruction_format="text")
|
||||
if "error" in drcts and "message" in drcts["error"] and drcts["error"]["message"]:
|
||||
raise IMException(drcts["error"]["message"])
|
||||
|
||||
if "routes" not in drcts or not drcts["routes"]:
|
||||
raise IMException("No route available for this trip")
|
||||
|
||||
myway = drcts["routes"][0]
|
||||
myway["summary"]["strduration"] = approx_duration(myway["summary"]["duration"])
|
||||
myway["summary"]["strdistance"] = approx_distance(myway["summary"]["distance"])
|
||||
res = Response("Trip summary: {strdistance} in approximate {strduration}; elevation +{ascent} m -{descent} m".format(**myway["summary"]), channel=msg.channel, count=" (%d more steps)", nomore="You have arrived!")
|
||||
|
||||
def formatSegments(segments):
|
||||
for segment in segments:
|
||||
for step in segment["steps"]:
|
||||
step["strtype"] = waytype[step["type"]]
|
||||
step["strduration"] = approx_duration(step["duration"])
|
||||
step["strdistance"] = approx_distance(step["distance"])
|
||||
yield "{instruction} for {strdistance} on {strtype} (approximate time: {strduration})".format(**step)
|
||||
|
||||
if "segments" in myway:
|
||||
res.append_message([m for m in formatSegments(myway["segments"])])
|
||||
|
||||
return res
|
||||
68
modules/pkgs.py
Normal file
68
modules/pkgs.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""Get information about common software"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import portage
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
DB = None
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def get_db():
|
||||
global DB
|
||||
if DB is None:
|
||||
DB = portage.db[portage.root]["porttree"].dbapi
|
||||
return DB
|
||||
|
||||
|
||||
def package_info(pkgname):
|
||||
pv = get_db().xmatch("match-all", pkgname)
|
||||
if not pv:
|
||||
raise IMException("No package named '%s' found" % pkgname)
|
||||
|
||||
bv = get_db().xmatch("bestmatch-visible", pkgname)
|
||||
pvsplit = portage.catpkgsplit(bv if bv else pv[-1])
|
||||
info = get_db().aux_get(bv if bv else pv[-1], ["DESCRIPTION", "HOMEPAGE", "LICENSE", "IUSE", "KEYWORDS"])
|
||||
|
||||
return {
|
||||
"pkgname": '/'.join(pvsplit[:2]),
|
||||
"category": pvsplit[0],
|
||||
"shortname": pvsplit[1],
|
||||
"lastvers": '-'.join(pvsplit[2:]) if pvsplit[3] != "r0" else pvsplit[2],
|
||||
"othersvers": ['-'.join(portage.catpkgsplit(p)[2:]) for p in pv if p != bv],
|
||||
"description": info[0],
|
||||
"homepage": info[1],
|
||||
"license": info[2],
|
||||
"uses": info[3],
|
||||
"keywords": info[4],
|
||||
}
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("eix",
|
||||
help="Get information about a package",
|
||||
help_usage={
|
||||
"NAME": "Get information about a software NAME"
|
||||
})
|
||||
def cmd_eix(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("please give me a package to search")
|
||||
|
||||
def srch(term):
|
||||
try:
|
||||
yield package_info(term)
|
||||
except portage.exception.AmbiguousPackageName as e:
|
||||
for i in e.args[0]:
|
||||
yield package_info(i)
|
||||
|
||||
res = Response(channel=msg.channel, count=" (%d more packages)", nomore="No more package '%s'" % msg.args[0])
|
||||
for pi in srch(msg.args[0]):
|
||||
res.append_message("\x03\x02{pkgname}:\x03\x02 {description} - {homepage} - {license} - last revisions: \x03\x02{lastvers}\x03\x02{ov}".format(ov=(", " + ', '.join(pi["othersvers"])) if pi["othersvers"] else "", **pi))
|
||||
return res
|
||||
74
modules/ratp.py
Normal file
74
modules/ratp.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"""Informe les usagers des prochains passages des transports en communs de la RATP"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from nextstop import ratp
|
||||
|
||||
@hook.command("ratp",
|
||||
help="Affiche les prochains horaires de passage",
|
||||
help_usage={
|
||||
"TRANSPORT": "Affiche les lignes du moyen de transport donné",
|
||||
"TRANSPORT LINE": "Affiche les stations sur la ligne de transport donnée",
|
||||
"TRANSPORT LINE STATION": "Affiche les prochains horaires de passage à l'arrêt donné",
|
||||
"TRANSPORT LINE STATION DESTINATION": "Affiche les prochains horaires de passage dans la direction donnée",
|
||||
})
|
||||
def ask_ratp(msg):
|
||||
l = len(msg.args)
|
||||
|
||||
transport = msg.args[0] if l > 0 else None
|
||||
line = msg.args[1] if l > 1 else None
|
||||
station = msg.args[2] if l > 2 else None
|
||||
direction = msg.args[3] if l > 3 else None
|
||||
|
||||
if station is not None:
|
||||
times = sorted(ratp.getNextStopsAtStation(transport, line, station, direction), key=lambda i: i[0])
|
||||
|
||||
if len(times) == 0:
|
||||
raise IMException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line))
|
||||
|
||||
(time, direction, stationname) = times[0]
|
||||
return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times],
|
||||
title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname),
|
||||
channel=msg.channel)
|
||||
|
||||
elif line is not None:
|
||||
stations = ratp.getAllStations(transport, line)
|
||||
|
||||
if len(stations) == 0:
|
||||
raise IMException("aucune station trouvée.")
|
||||
return Response(stations, title="Stations", channel=msg.channel)
|
||||
|
||||
elif transport is not None:
|
||||
lines = ratp.getTransportLines(transport)
|
||||
if len(lines) == 0:
|
||||
raise IMException("aucune ligne trouvée.")
|
||||
return Response(lines, title="Lignes", channel=msg.channel)
|
||||
|
||||
else:
|
||||
raise IMException("précise au moins un moyen de transport.")
|
||||
|
||||
|
||||
@hook.command("ratp_alert",
|
||||
help="Affiche les perturbations en cours sur le réseau")
|
||||
def ratp_alert(msg):
|
||||
if len(msg.args) == 0:
|
||||
raise IMException("précise au moins un moyen de transport.")
|
||||
|
||||
l = len(msg.args)
|
||||
transport = msg.args[0] if l > 0 else None
|
||||
line = msg.args[1] if l > 1 else None
|
||||
|
||||
if line is not None:
|
||||
d = ratp.getDisturbanceFromLine(transport, line)
|
||||
if "date" in d and d["date"] is not None:
|
||||
incidents = "Au {date[date]}, {title}: {message}".format(**d)
|
||||
else:
|
||||
incidents = "{title}: {message}".format(**d)
|
||||
else:
|
||||
incidents = ratp.getDisturbance(None, transport)
|
||||
|
||||
return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)")
|
||||
|
|
@ -4,13 +4,13 @@
|
|||
|
||||
import re
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
def help_full():
|
||||
|
|
@ -19,14 +19,14 @@ def help_full():
|
|||
LAST_SUBS = dict()
|
||||
|
||||
|
||||
@hook("cmd_hook", "subreddit")
|
||||
@hook.command("subreddit")
|
||||
def cmd_subreddit(msg):
|
||||
global LAST_SUBS
|
||||
if not len(msg.args):
|
||||
if msg.channel in LAST_SUBS and len(LAST_SUBS[msg.channel]) > 0:
|
||||
subs = [LAST_SUBS[msg.channel].pop()]
|
||||
else:
|
||||
raise IRCException("Which subreddit? Need inspiration? "
|
||||
raise IMException("Which subreddit? Need inspiration? "
|
||||
"type !horny or !bored")
|
||||
else:
|
||||
subs = msg.args
|
||||
|
|
@ -40,11 +40,11 @@ def cmd_subreddit(msg):
|
|||
else:
|
||||
where = "r"
|
||||
|
||||
sbr = web.getJSON("http://www.reddit.com/%s/%s/about.json" %
|
||||
sbr = web.getJSON("https://www.reddit.com/%s/%s/about.json" %
|
||||
(where, sub.group(2)))
|
||||
|
||||
if sbr is None:
|
||||
raise IRCException("subreddit not found")
|
||||
raise IMException("subreddit not found")
|
||||
|
||||
if "title" in sbr["data"]:
|
||||
res = Response(channel=msg.channel,
|
||||
|
|
@ -64,25 +64,32 @@ def cmd_subreddit(msg):
|
|||
channel=msg.channel))
|
||||
else:
|
||||
all_res.append(Response("%s is not a valid subreddit" % osub,
|
||||
channel=msg.channel, nick=msg.nick))
|
||||
channel=msg.channel, nick=msg.frm))
|
||||
|
||||
return all_res
|
||||
|
||||
|
||||
@hook("msg_default")
|
||||
@hook.message()
|
||||
def parselisten(msg):
|
||||
parseresponse(msg)
|
||||
return None
|
||||
global LAST_SUBS
|
||||
|
||||
if hasattr(msg, "message") and msg.message and type(msg.message) == str:
|
||||
urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.message)
|
||||
for url in urls:
|
||||
for recv in msg.to:
|
||||
if recv not in LAST_SUBS:
|
||||
LAST_SUBS[recv] = list()
|
||||
LAST_SUBS[recv].append(url)
|
||||
|
||||
|
||||
@hook("all_post")
|
||||
@hook.post()
|
||||
def parseresponse(msg):
|
||||
global LAST_SUBS
|
||||
|
||||
if hasattr(msg, "text") and msg.text:
|
||||
if hasattr(msg, "text") and msg.text and type(msg.text) == str:
|
||||
urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text)
|
||||
for url in urls:
|
||||
for recv in msg.receivers:
|
||||
for recv in msg.to:
|
||||
if recv not in LAST_SUBS:
|
||||
LAST_SUBS[recv] = list()
|
||||
LAST_SUBS[recv].append(url)
|
||||
|
|
|
|||
94
modules/repology.py
Normal file
94
modules/repology.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Repology.org module: the packaging hub"""
|
||||
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
nemubotversion = 4.0
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
URL_REPOAPI = "https://repology.org/api/v1/project/%s"
|
||||
|
||||
def get_json_project(project):
|
||||
prj = web.getJSON(URL_REPOAPI % (project))
|
||||
|
||||
return prj
|
||||
|
||||
|
||||
@hook.command("repology",
|
||||
help="Display version information about a package",
|
||||
help_usage={
|
||||
"PACKAGE_NAME": "Retrieve informations about PACKAGE_NAME",
|
||||
},
|
||||
keywords={
|
||||
"distro=DISTRO": "filter by disto",
|
||||
"status=STATUS[,STATUS...]": "filter by status",
|
||||
})
|
||||
def cmd_repology(msg):
|
||||
if len(msg.args) == 0:
|
||||
raise IMException("Please provide at least a package name")
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more information on package")
|
||||
|
||||
for project in msg.args:
|
||||
prj = get_json_project(project)
|
||||
if len(prj) == 0:
|
||||
raise IMException("Unable to find package " + project)
|
||||
|
||||
pkg_versions = {}
|
||||
pkg_maintainers = {}
|
||||
pkg_licenses = {}
|
||||
summary = None
|
||||
|
||||
for repo in prj:
|
||||
# Apply filters
|
||||
if "distro" in msg.kwargs and repo["repo"].find(msg.kwargs["distro"]) < 0:
|
||||
continue
|
||||
if "status" in msg.kwargs and repo["status"] not in msg.kwargs["status"].split(","):
|
||||
continue
|
||||
|
||||
name = repo["visiblename"] if "visiblename" in repo else repo["name"]
|
||||
status = repo["status"] if "status" in repo else "unknown"
|
||||
if name not in pkg_versions:
|
||||
pkg_versions[name] = {}
|
||||
if status not in pkg_versions[name]:
|
||||
pkg_versions[name][status] = []
|
||||
if repo["version"] not in pkg_versions[name][status]:
|
||||
pkg_versions[name][status].append(repo["version"])
|
||||
|
||||
if "maintainers" in repo:
|
||||
if name not in pkg_maintainers:
|
||||
pkg_maintainers[name] = []
|
||||
for maintainer in repo["maintainers"]:
|
||||
if maintainer not in pkg_maintainers[name]:
|
||||
pkg_maintainers[name].append(maintainer)
|
||||
|
||||
if "licenses" in repo:
|
||||
if name not in pkg_licenses:
|
||||
pkg_licenses[name] = []
|
||||
for lic in repo["licenses"]:
|
||||
if lic not in pkg_licenses[name]:
|
||||
pkg_licenses[name].append(lic)
|
||||
|
||||
if "summary" in repo and summary is None:
|
||||
summary = repo["summary"]
|
||||
|
||||
for pkgname in sorted(pkg_versions.keys()):
|
||||
m = "Package " + pkgname + " (" + summary + ")"
|
||||
if pkgname in pkg_licenses:
|
||||
m += " under " + ", ".join(pkg_licenses[pkgname])
|
||||
m += ": " + " - ".join([status + ": " + ", ".join(pkg_versions[pkgname][status]) for status in ["newest", "devel", "unique", "outdated", "legacy", "rolling", "noscheme", "untrusted", "ignored"] if status in pkg_versions[pkgname]])
|
||||
if "distro" in msg.kwargs and pkgname in pkg_maintainers:
|
||||
m += " - Maintained by " + ", ".join(pkg_maintainers[pkgname])
|
||||
|
||||
res.append_message(m)
|
||||
|
||||
return res
|
||||
|
|
@ -6,34 +6,49 @@ import random
|
|||
import shlex
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.message import Command
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook("cmd_hook", "choice")
|
||||
@hook.command("choice")
|
||||
def cmd_choice(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate some terms to pick!")
|
||||
raise IMException("indicate some terms to pick!")
|
||||
|
||||
return Response(random.choice(msg.args),
|
||||
channel=msg.channel,
|
||||
nick=msg.nick)
|
||||
nick=msg.frm)
|
||||
|
||||
|
||||
@hook("cmd_hook", "choicecmd")
|
||||
def cmd_choice(msg):
|
||||
@hook.command("choicecmd")
|
||||
def cmd_choicecmd(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate some command to pick!")
|
||||
raise IMException("indicate some command to pick!")
|
||||
|
||||
choice = shlex.split(random.choice(msg.args))
|
||||
|
||||
return [x for x in context.subtreat(Command(choice[0][1:],
|
||||
choice[1:],
|
||||
to_response=msg.to_response,
|
||||
frm=msg.frm,
|
||||
server=msg.server))]
|
||||
return [x for x in context.subtreat(context.subparse(msg, choice))]
|
||||
|
||||
|
||||
@hook.command("choiceres")
|
||||
def cmd_choiceres(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("indicate some command to pick a message from!")
|
||||
|
||||
rl = [x for x in context.subtreat(context.subparse(msg, " ".join(msg.args)))]
|
||||
if len(rl) <= 0:
|
||||
return rl
|
||||
|
||||
r = random.choice(rl)
|
||||
|
||||
if isinstance(r, Response):
|
||||
for i in range(len(r.messages) - 1, -1, -1):
|
||||
if isinstance(r.messages[i], list):
|
||||
r.messages = [ random.choice(random.choice(r.messages)) ]
|
||||
elif isinstance(r.messages[i], str):
|
||||
r.messages = [ random.choice(r.messages) ]
|
||||
return r
|
||||
|
|
|
|||
|
|
@ -2,32 +2,30 @@
|
|||
|
||||
"""Find information about an SAP transaction codes"""
|
||||
|
||||
import re
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
from nemubot.tools.web import striphtml
|
||||
|
||||
nemubotversion = 4.0
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
def help_full():
|
||||
return "Retrieve SAP transaction codes and details using tcodes or keywords: !tcode <transaction code|keywords>"
|
||||
|
||||
|
||||
@hook("cmd_hook", "tcode")
|
||||
@hook.command("tcode")
|
||||
def cmd_tcode(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate a transaction code or "
|
||||
raise IMException("indicate a transaction code or "
|
||||
"a keyword to search!")
|
||||
|
||||
url = ("http://www.tcodesearch.com/tcodes/search?q=%s" %
|
||||
url = ("https://www.tcodesearch.com/tcodes/search?q=%s" %
|
||||
urllib.parse.quote(msg.args[0]))
|
||||
|
||||
page = web.getURLContent(url)
|
||||
|
|
|
|||
104
modules/shodan.py
Normal file
104
modules/shodan.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Search engine for IoT"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
from datetime import datetime
|
||||
import ipaddress
|
||||
import urllib.parse
|
||||
|
||||
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
|
||||
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
BASEURL = "https://api.shodan.io/shodan/"
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
if not context.config or "apikey" not in context.config:
|
||||
raise ImportError("You need a Shodan API key in order to use this "
|
||||
"module. Add it to the module configuration file:\n"
|
||||
"<module name=\"shodan\" apikey=\"XXXXXXXXXXXXXXXX\" "
|
||||
"/>\nRegister at https://account.shodan.io/register")
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def host_lookup(ip):
|
||||
url = BASEURL + "host/" + urllib.parse.quote(ip) + "?" + urllib.parse.urlencode({'key': context.config["apikey"]})
|
||||
return web.getJSON(url)
|
||||
|
||||
|
||||
def search_hosts(query):
|
||||
url = BASEURL + "host/search?" + urllib.parse.urlencode({'query': query, 'key': context.config["apikey"]})
|
||||
return web.getJSON(url, max_size=4194304)
|
||||
|
||||
|
||||
def print_ssl(ssl):
|
||||
return (
|
||||
"SSL: " +
|
||||
" ".join([v for v in ssl["versions"] if v[0] != "-"]) +
|
||||
"; cipher used: " + ssl["cipher"]["name"] +
|
||||
("; certificate: " + ssl["cert"]["sig_alg"] +
|
||||
" issued by: " + ssl["cert"]["issuer"]["CN"] +
|
||||
" expires on: " + str(datetime.strptime(ssl["cert"]["expires"], "%Y%m%d%H%M%SZ")) if "cert" in ssl else "")
|
||||
)
|
||||
|
||||
def print_service(svc):
|
||||
ip = ipaddress.ip_address(svc["ip_str"])
|
||||
return ((svc["ip_str"] if ip.version == 4 else "[%s]" % svc["ip_str"]) +
|
||||
":{port}/{transport} ({module}):" +
|
||||
(" {os}" if svc["os"] else "") +
|
||||
(" {product}" if "product" in svc else "") +
|
||||
(" {version}" if "version" in svc else "") +
|
||||
(" {info}" if "info" in svc else "") +
|
||||
(" Vulns: " + ", ".join(svc["opts"]["vulns"]) if "opts" in svc and "vulns" in svc["opts"] else "") +
|
||||
(" " + print_ssl(svc["ssl"]) if "ssl" in svc else "") +
|
||||
(" \x03\x1D" + svc["data"].replace("\r\n", "\n").split("\n")[0] + "\x03\x1D" if "data" in svc else "") +
|
||||
(" " + svc["title"] if "title" in svc else "")
|
||||
).format(module=svc["_shodan"]["module"], **svc)
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("shodan",
|
||||
help="Use shodan.io to get information on machines connected to Internet",
|
||||
help_usage={
|
||||
"IP": "retrieve information about the given IP (can be v4 or v6)",
|
||||
"TERM": "retrieve all hosts matching TERM somewhere in their exposed stuff"
|
||||
})
|
||||
def shodan(msg):
|
||||
if not msg.args:
|
||||
raise IMException("indicate an IP or a term to search!")
|
||||
|
||||
terms = " ".join(msg.args)
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(terms)
|
||||
except ValueError:
|
||||
ip = None
|
||||
|
||||
if ip:
|
||||
h = host_lookup(terms)
|
||||
res = Response(channel=msg.channel,
|
||||
title="%s" % ((h["ip_str"] if ip.version == 4 else "[%s]" % h["ip_str"]) + (" (" + ", ".join(h["hostnames"]) + ")") if h["hostnames"] else ""))
|
||||
res.append_message("{isp} ({asn}) -> {city} ({country_code}), running {os}. Vulns: {vulns_str}. Open ports: {open_ports}. Last update: {last_update}".format(
|
||||
open_ports=", ".join(map(lambda a: str(a), h["ports"])), vulns_str=", ".join(h["vulns"]) if "vulns" in h else None, **h).strip())
|
||||
for d in h["data"]:
|
||||
res.append_message(print_service(d))
|
||||
|
||||
else:
|
||||
q = search_hosts(terms)
|
||||
res = Response(channel=msg.channel,
|
||||
count=" (%%s/%s results)" % q["total"])
|
||||
for r in q["matches"]:
|
||||
res.append_message(print_service(r))
|
||||
|
||||
return res
|
||||
|
|
@ -10,7 +10,7 @@ from nemubot.hooks import hook
|
|||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
def help_full():
|
||||
|
|
@ -19,7 +19,7 @@ def help_full():
|
|||
" hh:mm")
|
||||
|
||||
|
||||
@hook("cmd_hook", "sleepytime")
|
||||
@hook.command("sleepytime")
|
||||
def cmd_sleep(msg):
|
||||
if len(msg.args) and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?",
|
||||
msg.args[0]) is not None:
|
||||
|
|
|
|||
116
modules/smmry.py
Normal file
116
modules/smmry.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""Summarize texts"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
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
|
||||
from nemubot.module.urlreducer import LAST_URLS
|
||||
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
URL_API = "https://api.smmry.com/?SM_API_KEY=%s"
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
if not context.config or "apikey" not in context.config:
|
||||
raise ImportError("You need a Smmry API key in order to use this "
|
||||
"module. Add it to the module configuration file:\n"
|
||||
"<module name=\"smmry\" apikey=\"XXXXXXXXXXXXXXXX\" "
|
||||
"/>\nRegister at https://smmry.com/partner")
|
||||
global URL_API
|
||||
URL_API = URL_API % context.config["apikey"]
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("smmry",
|
||||
help="Summarize the following words/command return",
|
||||
help_usage={
|
||||
"WORDS/CMD": ""
|
||||
},
|
||||
keywords={
|
||||
"keywords?=X": "Returns keywords instead of summary (count optional)",
|
||||
"length=7": "The number of sentences returned, default 7",
|
||||
"break": "inserts the string [BREAK] between sentences",
|
||||
"ignore_length": "returns summary regardless of quality or length",
|
||||
"quote_avoid": "sentences with quotations will be excluded",
|
||||
"question_avoid": "sentences with question will be excluded",
|
||||
"exclamation_avoid": "sentences with exclamation marks will be excluded",
|
||||
})
|
||||
def cmd_smmry(msg):
|
||||
if not len(msg.args):
|
||||
global LAST_URLS
|
||||
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
|
||||
msg.args.append(LAST_URLS[msg.channel].pop())
|
||||
else:
|
||||
raise IMException("I have no more URL to sum up.")
|
||||
|
||||
URL = URL_API
|
||||
if "length" in msg.kwargs:
|
||||
if int(msg.kwargs["length"]) > 0 :
|
||||
URL += "&SM_LENGTH=" + msg.kwargs["length"]
|
||||
else:
|
||||
msg.kwargs["ignore_length"] = True
|
||||
if "break" in msg.kwargs: URL += "&SM_WITH_BREAK"
|
||||
if "ignore_length" in msg.kwargs: URL += "&SM_IGNORE_LENGTH"
|
||||
if "quote_avoid" in msg.kwargs: URL += "&SM_QUOTE_AVOID"
|
||||
if "question_avoid" in msg.kwargs: URL += "&SM_QUESTION_AVOID"
|
||||
if "exclamation_avoid" in msg.kwargs: URL += "&SM_EXCLAMATION_AVOID"
|
||||
if "keywords" in msg.kwargs and msg.kwargs["keywords"] is not None and int(msg.kwargs["keywords"]) > 0: URL += "&SM_KEYWORD_COUNT=" + msg.kwargs["keywords"]
|
||||
|
||||
res = Response(channel=msg.channel)
|
||||
|
||||
if web.isURL(" ".join(msg.args)):
|
||||
smmry = web.getJSON(URL + "&SM_URL=" + quote(" ".join(msg.args)), timeout=23)
|
||||
else:
|
||||
cnt = ""
|
||||
for r in context.subtreat(context.subparse(msg, " ".join(msg.args))):
|
||||
if isinstance(r, Response):
|
||||
for i in range(len(r.messages) - 1, -1, -1):
|
||||
if isinstance(r.messages[i], list):
|
||||
for j in range(len(r.messages[i]) - 1, -1, -1):
|
||||
cnt += r.messages[i][j] + "\n"
|
||||
elif isinstance(r.messages[i], str):
|
||||
cnt += r.messages[i] + "\n"
|
||||
else:
|
||||
cnt += str(r.messages) + "\n"
|
||||
|
||||
elif isinstance(r, Text):
|
||||
cnt += r.message + "\n"
|
||||
|
||||
else:
|
||||
cnt += str(r) + "\n"
|
||||
|
||||
smmry = web.getJSON(URL, body="sm_api_input=" + quote(cnt), timeout=23)
|
||||
|
||||
if "sm_api_error" in smmry:
|
||||
if smmry["sm_api_error"] == 0:
|
||||
title = "Internal server problem (not your fault)"
|
||||
elif smmry["sm_api_error"] == 1:
|
||||
title = "Incorrect submission variables"
|
||||
elif smmry["sm_api_error"] == 2:
|
||||
title = "Intentional restriction (low credits?)"
|
||||
elif smmry["sm_api_error"] == 3:
|
||||
title = "Summarization error"
|
||||
else:
|
||||
title = "Unknown error"
|
||||
raise IMException(title + ": " + smmry['sm_api_message'].lower())
|
||||
|
||||
if "keywords" in msg.kwargs:
|
||||
smmry["sm_api_content"] = ", ".join(smmry["sm_api_keyword_array"])
|
||||
|
||||
if "sm_api_title" in smmry and smmry["sm_api_title"] != "":
|
||||
res.append_message(smmry["sm_api_content"], title=smmry["sm_api_title"])
|
||||
else:
|
||||
res.append_message(smmry["sm_api_content"])
|
||||
|
||||
return res
|
||||
110
modules/sms.py
110
modules/sms.py
|
|
@ -10,13 +10,13 @@ import urllib.request
|
|||
import urllib.parse
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
def load(context):
|
||||
context.data.setIndex("name", "phone")
|
||||
|
|
@ -46,47 +46,89 @@ def send_sms(frm, api_usr, api_key, content):
|
|||
|
||||
return None
|
||||
|
||||
|
||||
@hook("cmd_hook", "sms")
|
||||
def cmd_sms(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("À qui veux-tu envoyer ce SMS ?")
|
||||
|
||||
# Check dests
|
||||
cur_epoch = time.mktime(time.localtime());
|
||||
for u in msg.args[0].split(","):
|
||||
def check_sms_dests(dests, cur_epoch):
|
||||
"""Raise exception if one of the dest is not known or has already receive a SMS recently
|
||||
"""
|
||||
for u in dests:
|
||||
if u not in context.data.index:
|
||||
raise IRCException("Désolé, je sais pas comment envoyer de SMS à %s." % u)
|
||||
raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u)
|
||||
elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42:
|
||||
raise IRCException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u)
|
||||
raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u)
|
||||
return True
|
||||
|
||||
# Go!
|
||||
|
||||
def send_sms_to_list(msg, frm, dests, content, cur_epoch):
|
||||
fails = list()
|
||||
for u in msg.args[0].split(","):
|
||||
for u in dests:
|
||||
context.data.index[u]["lastuse"] = cur_epoch
|
||||
if msg.to_response[0] == msg.frm:
|
||||
frm = msg.frm
|
||||
else:
|
||||
frm = msg.frm + "@" + msg.to[0]
|
||||
test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], " ".join(msg.args[1:]))
|
||||
test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], content)
|
||||
if test is not None:
|
||||
fails.append( "%s: %s" % (u, test) )
|
||||
|
||||
if len(fails) > 0:
|
||||
return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.nick)
|
||||
return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.frm)
|
||||
else:
|
||||
return Response("le SMS a bien été envoyé", msg.channel, msg.nick)
|
||||
return Response("le SMS a bien été envoyé", msg.channel, msg.frm)
|
||||
|
||||
|
||||
@hook.command("sms")
|
||||
def cmd_sms(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("À qui veux-tu envoyer ce SMS ?")
|
||||
|
||||
cur_epoch = time.mktime(time.localtime())
|
||||
dests = msg.args[0].split(",")
|
||||
frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0]
|
||||
content = " ".join(msg.args[1:])
|
||||
|
||||
check_sms_dests(dests, cur_epoch)
|
||||
return send_sms_to_list(msg, frm, dests, content, cur_epoch)
|
||||
|
||||
|
||||
@hook.command("smscmd")
|
||||
def cmd_smscmd(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("À qui veux-tu envoyer ce SMS ?")
|
||||
|
||||
cur_epoch = time.mktime(time.localtime())
|
||||
dests = msg.args[0].split(",")
|
||||
frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0]
|
||||
cmd = " ".join(msg.args[1:])
|
||||
|
||||
content = None
|
||||
for r in context.subtreat(context.subparse(msg, cmd)):
|
||||
if isinstance(r, Response):
|
||||
for m in r.messages:
|
||||
if isinstance(m, list):
|
||||
for n in m:
|
||||
content = n
|
||||
break
|
||||
if content is not None:
|
||||
break
|
||||
elif isinstance(m, str):
|
||||
content = m
|
||||
break
|
||||
|
||||
elif isinstance(r, Text):
|
||||
content = r.message
|
||||
|
||||
if content is None:
|
||||
raise IMException("Aucun SMS envoyé : le résultat de la commande n'a pas retourné de contenu.")
|
||||
|
||||
check_sms_dests(dests, cur_epoch)
|
||||
return send_sms_to_list(msg, frm, dests, content, cur_epoch)
|
||||
|
||||
|
||||
apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P<user>[0-9]{7,})", re.IGNORECASE)
|
||||
apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P<key>[a-zA-Z0-9]{10,})", re.IGNORECASE)
|
||||
|
||||
@hook("ask_default")
|
||||
@hook.ask()
|
||||
def parseask(msg):
|
||||
if msg.text.find("Free") >= 0 and (
|
||||
msg.text.find("API") >= 0 or msg.text.find("api") >= 0) and (
|
||||
msg.text.find("SMS") >= 0 or msg.text.find("sms") >= 0):
|
||||
resuser = apiuser_ask.search(msg.text)
|
||||
reskey = apikey_ask.search(msg.text)
|
||||
if msg.message.find("Free") >= 0 and (
|
||||
msg.message.find("API") >= 0 or msg.message.find("api") >= 0) and (
|
||||
msg.message.find("SMS") >= 0 or msg.message.find("sms") >= 0):
|
||||
resuser = apiuser_ask.search(msg.message)
|
||||
reskey = apikey_ask.search(msg.message)
|
||||
if resuser is not None and reskey is not None:
|
||||
apiuser = resuser.group("user")
|
||||
apikey = reskey.group("key")
|
||||
|
|
@ -94,18 +136,18 @@ def parseask(msg):
|
|||
test = send_sms("nemubot", apiuser, apikey,
|
||||
"Vous avez enregistré vos codes d'authentification dans nemubot, félicitation !")
|
||||
if test is not None:
|
||||
return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.nick)
|
||||
return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.frm)
|
||||
|
||||
if msg.nick in context.data.index:
|
||||
context.data.index[msg.nick]["user"] = apiuser
|
||||
context.data.index[msg.nick]["key"] = apikey
|
||||
if msg.frm in context.data.index:
|
||||
context.data.index[msg.frm]["user"] = apiuser
|
||||
context.data.index[msg.frm]["key"] = apikey
|
||||
else:
|
||||
ms = ModuleState("phone")
|
||||
ms.setAttribute("name", msg.nick)
|
||||
ms.setAttribute("name", msg.frm)
|
||||
ms.setAttribute("user", apiuser)
|
||||
ms.setAttribute("key", apikey)
|
||||
ms.setAttribute("lastuse", 0)
|
||||
context.data.addChild(ms)
|
||||
context.save()
|
||||
return Response("ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)",
|
||||
msg.channel, msg.nick)
|
||||
msg.channel, msg.frm)
|
||||
|
|
|
|||
|
|
@ -1,53 +1,25 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Check words spelling"""
|
||||
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
from .pyaspell import Aspell
|
||||
from .pyaspell import AspellError
|
||||
|
||||
nemubotversion = 3.4
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from more import Response
|
||||
|
||||
def help_full():
|
||||
return "!spell [<lang>] <word>: give the correct spelling of <word> in <lang=fr>."
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
context.data.setIndex("name", "score")
|
||||
|
||||
@hook("cmd_hook", "spell")
|
||||
def cmd_spell(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.")
|
||||
|
||||
lang = "fr"
|
||||
strRes = list()
|
||||
for word in msg.args:
|
||||
if len(word) <= 2 and len(msg.args) > 2:
|
||||
lang = word
|
||||
else:
|
||||
try:
|
||||
r = check_spell(word, lang)
|
||||
except AspellError:
|
||||
return Response("Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel, msg.nick)
|
||||
if r == True:
|
||||
add_score(msg.nick, "correct")
|
||||
strRes.append("l'orthographe de `%s' est correcte" % word)
|
||||
elif len(r) > 0:
|
||||
add_score(msg.nick, "bad")
|
||||
strRes.append("suggestions pour `%s' : %s" % (word, ", ".join(r)))
|
||||
else:
|
||||
add_score(msg.nick, "bad")
|
||||
strRes.append("aucune suggestion pour `%s'" % word)
|
||||
return Response(strRes, channel=msg.channel, nick=msg.nick)
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def add_score(nick, t):
|
||||
if nick not in context.data.index:
|
||||
|
|
@ -61,21 +33,6 @@ def add_score(nick, t):
|
|||
context.data.index[nick][t] = 1
|
||||
context.save()
|
||||
|
||||
@hook("cmd_hook", "spellscore")
|
||||
def cmd_score(msg):
|
||||
res = list()
|
||||
unknown = list()
|
||||
if not len(msg.args):
|
||||
raise IRCException("De qui veux-tu voir les scores ?")
|
||||
for cmd in msg.args:
|
||||
if cmd in context.data.index:
|
||||
res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, context.data.index[cmd].getInt(a)) for a in context.data.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel))
|
||||
else:
|
||||
unknown.append(cmd)
|
||||
if len(unknown) > 0:
|
||||
res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel))
|
||||
|
||||
return res
|
||||
|
||||
def check_spell(word, lang='fr'):
|
||||
a = Aspell([("lang", lang)])
|
||||
|
|
@ -85,3 +42,56 @@ def check_spell(word, lang='fr'):
|
|||
ret = a.suggest(word.encode("utf-8"))
|
||||
a.close()
|
||||
return ret
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("spell",
|
||||
help="give the correct spelling of given words",
|
||||
help_usage={"WORD": "give the correct spelling of the WORD."},
|
||||
keywords={"lang=": "change the language use for checking, default fr"})
|
||||
def cmd_spell(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.")
|
||||
|
||||
lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr"
|
||||
|
||||
res = Response(channel=msg.channel)
|
||||
for word in msg.args:
|
||||
try:
|
||||
r = check_spell(word, lang)
|
||||
except AspellError:
|
||||
raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang)
|
||||
|
||||
if r == True:
|
||||
add_score(msg.frm, "correct")
|
||||
res.append_message("l'orthographe de `%s' est correcte" % word)
|
||||
|
||||
elif len(r) > 0:
|
||||
add_score(msg.frm, "bad")
|
||||
res.append_message(r, title="suggestions pour `%s'" % word)
|
||||
|
||||
else:
|
||||
add_score(msg.frm, "bad")
|
||||
res.append_message("aucune suggestion pour `%s'" % word)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@hook.command("spellscore",
|
||||
help="Show spell score (tests, mistakes, ...) for someone",
|
||||
help_usage={"USER": "Display score of USER"})
|
||||
def cmd_score(msg):
|
||||
res = list()
|
||||
unknown = list()
|
||||
if not len(msg.args):
|
||||
raise IMException("De qui veux-tu voir les scores ?")
|
||||
for cmd in msg.args:
|
||||
if cmd in context.data.index:
|
||||
res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, context.data.index[cmd].getInt(a)) for a in context.data.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel))
|
||||
else:
|
||||
unknown.append(cmd)
|
||||
if len(unknown) > 0:
|
||||
res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel))
|
||||
|
||||
return res
|
||||
|
|
|
|||
382
modules/suivi.py
382
modules/suivi.py
|
|
@ -1,32 +1,53 @@
|
|||
import urllib.request
|
||||
"""Postal tracking module"""
|
||||
|
||||
# PYTHON STUFF ############################################
|
||||
|
||||
import json
|
||||
import urllib.parse
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.tools.web import getURLContent
|
||||
from more import Response
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.tools.web import getURLContent, getURLHeaders, getJSON
|
||||
from nemubot.module.more import Response
|
||||
|
||||
nemubotversion = 4.0
|
||||
|
||||
def help_full():
|
||||
return "Traquez vos courriers La Poste ou Colissimo en utilisant la commande: !laposte <tracking number> ou !colissimo <tracking number>\nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index et http://www.colissimo.fr/portail_colissimo/suivre.do"
|
||||
# POSTAGE SERVICE PARSERS ############################################
|
||||
|
||||
def get_tnt_info(track_id):
|
||||
values = []
|
||||
data = getURLContent('https://www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id)
|
||||
soup = BeautifulSoup(data)
|
||||
status_list = soup.find('div', class_='result__content')
|
||||
if not status_list:
|
||||
return None
|
||||
last_status = status_list.find('div', class_='roster')
|
||||
if last_status:
|
||||
for info in last_status.find_all('div', class_='roster__item'):
|
||||
values.append(info.get_text().strip())
|
||||
if len(values) == 3:
|
||||
return (values[0], values[1], values[2])
|
||||
|
||||
|
||||
def get_colissimo_info(colissimo_id):
|
||||
colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id)
|
||||
colissimo_data = getURLContent("https://www.laposte.fr/particulier/outils/suivre-vos-envois?code=%s" % colissimo_id)
|
||||
soup = BeautifulSoup(colissimo_data)
|
||||
|
||||
dataArray = soup.find(class_='dataArray')
|
||||
if dataArray and dataArray.tbody and dataArray.tbody.tr:
|
||||
date = dataArray.tbody.tr.find(headers="Date").get_text()
|
||||
libelle = dataArray.tbody.tr.find(headers="Libelle").get_text().replace('\n', '').replace('\t', '').replace('\r', '')
|
||||
site = dataArray.tbody.tr.find(headers="site").get_text().strip()
|
||||
return (date, libelle, site.strip())
|
||||
dataArray = soup.find(class_='results-suivi')
|
||||
if dataArray and dataArray.table and dataArray.table.tbody and dataArray.table.tbody.tr:
|
||||
td = dataArray.table.tbody.tr.find_all('td')
|
||||
if len(td) > 2:
|
||||
date = td[0].get_text()
|
||||
libelle = re.sub(r'[\n\t\r]', '', td[1].get_text())
|
||||
site = td[2].get_text().strip()
|
||||
return (date, libelle, site.strip())
|
||||
|
||||
|
||||
def get_chronopost_info(track_id):
|
||||
data = urllib.parse.urlencode({'listeNumeros': track_id})
|
||||
track_baseurl = "http://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR"
|
||||
track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8'))
|
||||
track_baseurl = "https://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR"
|
||||
track_data = getURLContent(track_baseurl, data.encode('utf-8'))
|
||||
soup = BeautifulSoup(track_data)
|
||||
|
||||
infoClass = soup.find(class_='numeroColi2')
|
||||
|
|
@ -39,104 +60,273 @@ def get_chronopost_info(track_id):
|
|||
libelle = info[1]
|
||||
return (date, libelle)
|
||||
|
||||
|
||||
def get_colisprive_info(track_id):
|
||||
data = urllib.parse.urlencode({'numColis': track_id})
|
||||
track_baseurl = "https://www.colisprive.com/moncolis/pages/detailColis.aspx"
|
||||
track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8'))
|
||||
track_data = getURLContent(track_baseurl, data.encode('utf-8'))
|
||||
soup = BeautifulSoup(track_data)
|
||||
|
||||
dataArray = soup.find(class_='BandeauInfoColis')
|
||||
if dataArray and dataArray.find(class_='divStatut') and dataArray.find(class_='divStatut').find(class_='tdText'):
|
||||
status = dataArray.find(class_='divStatut').find(class_='tdText').get_text()
|
||||
if (dataArray and dataArray.find(class_='divStatut')
|
||||
and dataArray.find(class_='divStatut').find(class_='tdText')):
|
||||
status = dataArray.find(class_='divStatut') \
|
||||
.find(class_='tdText').get_text()
|
||||
return status
|
||||
|
||||
|
||||
def get_ups_info(track_id):
|
||||
data = json.dumps({'Locale': "en_US", 'TrackingNumber': [track_id]})
|
||||
track_baseurl = "https://www.ups.com/track/api/Track/GetStatus?loc=en_US"
|
||||
track_data = getJSON(track_baseurl, data.encode('utf-8'), header={"Content-Type": "application/json"})
|
||||
return (track_data["trackDetails"][0]["trackingNumber"],
|
||||
track_data["trackDetails"][0]["packageStatus"],
|
||||
track_data["trackDetails"][0]["shipmentProgressActivities"][0]["date"] + " " + track_data["trackDetails"][0]["shipmentProgressActivities"][0]["time"],
|
||||
track_data["trackDetails"][0]["shipmentProgressActivities"][0]["location"],
|
||||
track_data["trackDetails"][0]["shipmentProgressActivities"][0]["activityScan"])
|
||||
|
||||
|
||||
def get_laposte_info(laposte_id):
|
||||
data = urllib.parse.urlencode({'id': laposte_id})
|
||||
laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index"
|
||||
status, laposte_headers = getURLHeaders("https://www.laposte.fr/outils/suivre-vos-envois?" + urllib.parse.urlencode({'code': laposte_id}))
|
||||
|
||||
laposte_data = urllib.request.urlopen(laposte_baseurl, data.encode('utf-8'))
|
||||
soup = BeautifulSoup(laposte_data)
|
||||
search_res = soup.find(class_='resultat_rech_simple_table').tbody.tr
|
||||
if (soup.find(class_='resultat_rech_simple_table').thead
|
||||
and soup.find(class_='resultat_rech_simple_table').thead.tr
|
||||
and len(search_res.find_all('td')) > 3):
|
||||
field = search_res.find('td')
|
||||
poste_id = field.get_text()
|
||||
laposte_cookie = None
|
||||
for k,v in laposte_headers:
|
||||
if k.lower() == "set-cookie" and v.find("access_token") >= 0:
|
||||
laposte_cookie = v.split(";")[0]
|
||||
|
||||
field = field.find_next('td')
|
||||
poste_type = field.get_text()
|
||||
laposte_data = getJSON("https://api.laposte.fr/ssu/v1/suivi-unifie/idship/%s?lang=fr_FR" % urllib.parse.quote(laposte_id), header={"Accept": "application/json", "Cookie": laposte_cookie})
|
||||
|
||||
field = field.find_next('td')
|
||||
poste_date = field.get_text()
|
||||
shipment = laposte_data["shipment"]
|
||||
return (shipment["product"], shipment["idShip"], shipment["event"][0]["label"], shipment["event"][0]["date"])
|
||||
|
||||
field = field.find_next('td')
|
||||
poste_location = field.get_text()
|
||||
|
||||
field = field.find_next('td')
|
||||
poste_status = field.get_text()
|
||||
def get_postnl_info(postnl_id):
|
||||
data = urllib.parse.urlencode({'barcodes': postnl_id})
|
||||
postnl_baseurl = "http://www.postnl.post/details/"
|
||||
|
||||
return (poste_type.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date)
|
||||
postnl_data = getURLContent(postnl_baseurl, data.encode('utf-8'))
|
||||
soup = BeautifulSoup(postnl_data)
|
||||
if (soup.find(id='datatables')
|
||||
and soup.find(id='datatables').tbody
|
||||
and soup.find(id='datatables').tbody.tr):
|
||||
search_res = soup.find(id='datatables').tbody.tr
|
||||
if len(search_res.find_all('td')) >= 3:
|
||||
field = field.find_next('td')
|
||||
post_date = field.get_text()
|
||||
|
||||
@hook("cmd_hook", "track")
|
||||
field = field.find_next('td')
|
||||
post_status = field.get_text()
|
||||
|
||||
field = field.find_next('td')
|
||||
post_destination = field.get_text()
|
||||
|
||||
return (post_status.lower(), post_destination, post_date)
|
||||
|
||||
|
||||
def get_usps_info(usps_id):
|
||||
usps_parcelurl = "https://tools.usps.com/go/TrackConfirmAction_input?" + urllib.parse.urlencode({'qtc_tLabels1': usps_id})
|
||||
|
||||
usps_data = getURLContent(usps_parcelurl)
|
||||
soup = BeautifulSoup(usps_data)
|
||||
if (soup.find(id="trackingHistory_1")
|
||||
and soup.find(class_="tracking_history").find(class_="row_notification")
|
||||
and soup.find(class_="tracking_history").find(class_="row_top").find_all("td")):
|
||||
notification = soup.find(class_="tracking_history").find(class_="row_notification").text.strip()
|
||||
date = re.sub(r"\s+", " ", soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[0].text.strip())
|
||||
status = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[1].text.strip()
|
||||
last_location = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[2].text.strip()
|
||||
|
||||
print(notification)
|
||||
|
||||
return (notification, date, status, last_location)
|
||||
|
||||
|
||||
def get_fedex_info(fedex_id, lang="en_US"):
|
||||
data = urllib.parse.urlencode({
|
||||
'data': json.dumps({
|
||||
"TrackPackagesRequest": {
|
||||
"appType": "WTRK",
|
||||
"appDeviceType": "DESKTOP",
|
||||
"uniqueKey": "",
|
||||
"processingParameters": {},
|
||||
"trackingInfoList": [
|
||||
{
|
||||
"trackNumberInfo": {
|
||||
"trackingNumber": str(fedex_id),
|
||||
"trackingQualifier": "",
|
||||
"trackingCarrier": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
'action': "trackpackages",
|
||||
'locale': lang,
|
||||
'version': 1,
|
||||
'format': "json"
|
||||
})
|
||||
fedex_baseurl = "https://www.fedex.com/trackingCal/track"
|
||||
|
||||
fedex_data = getJSON(fedex_baseurl, data.encode('utf-8'))
|
||||
|
||||
if ("TrackPackagesResponse" in fedex_data and
|
||||
"packageList" in fedex_data["TrackPackagesResponse"] and
|
||||
len(fedex_data["TrackPackagesResponse"]["packageList"]) and
|
||||
(not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] or
|
||||
fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] == '0') and
|
||||
not fedex_data["TrackPackagesResponse"]["packageList"][0]["errorList"][0]["code"]
|
||||
):
|
||||
return fedex_data["TrackPackagesResponse"]["packageList"][0]
|
||||
|
||||
|
||||
def get_dhl_info(dhl_id, lang="en"):
|
||||
dhl_parcelurl = "http://www.dhl.com/shipmentTracking?" + urllib.parse.urlencode({'AWB': dhl_id})
|
||||
|
||||
dhl_data = getJSON(dhl_parcelurl)
|
||||
|
||||
if "results" in dhl_data and dhl_data["results"]:
|
||||
return dhl_data["results"][0]
|
||||
|
||||
|
||||
# TRACKING HANDLERS ###################################################
|
||||
|
||||
def handle_tnt(tracknum):
|
||||
info = get_tnt_info(tracknum)
|
||||
if info:
|
||||
status, date, place = info
|
||||
placestr = ''
|
||||
if place:
|
||||
placestr = ' à \x02{place}\x0f'
|
||||
return ('Le colis \x02{trackid}\x0f a actuellement le status: '
|
||||
'\x02{status}\x0F mis à jour le \x02{date}\x0f{place}.'
|
||||
.format(trackid=tracknum, status=status,
|
||||
date=re.sub(r'\s+', ' ', date), place=placestr))
|
||||
|
||||
|
||||
def handle_laposte(tracknum):
|
||||
info = get_laposte_info(tracknum)
|
||||
if info:
|
||||
poste_type, poste_id, poste_status, poste_date = info
|
||||
return ("\x02%s\x0F : \x02%s\x0F est actuellement "
|
||||
"\x02%s\x0F (Mis à jour le \x02%s\x0F"
|
||||
")." % (poste_type, poste_id, poste_status, poste_date))
|
||||
|
||||
|
||||
def handle_postnl(tracknum):
|
||||
info = get_postnl_info(tracknum)
|
||||
if info:
|
||||
post_status, post_destination, post_date = info
|
||||
return ("PostNL \x02%s\x0F est actuellement "
|
||||
"\x02%s\x0F vers le pays \x02%s\x0F (Mis à jour le \x02%s\x0F"
|
||||
")." % (tracknum, post_status, post_destination, post_date))
|
||||
|
||||
|
||||
def handle_usps(tracknum):
|
||||
info = get_usps_info(tracknum)
|
||||
if info:
|
||||
notif, last_date, last_status, last_location = info
|
||||
return ("USPS \x02{tracknum}\x0F: {last_status} in \x02{last_location}\x0F as of {last_date}: {notif}".format(tracknum=tracknum, notif=notif, last_date=last_date, last_status=last_status.lower(), last_location=last_location))
|
||||
|
||||
|
||||
def handle_ups(tracknum):
|
||||
info = get_ups_info(tracknum)
|
||||
if info:
|
||||
tracknum, status, last_date, last_location, last_status = info
|
||||
return ("UPS \x02{tracknum}\x0F: {status}: in \x02{last_location}\x0F as of {last_date}: {last_status}".format(tracknum=tracknum, status=status, last_date=last_date, last_status=last_status.lower(), last_location=last_location))
|
||||
|
||||
|
||||
def handle_colissimo(tracknum):
|
||||
info = get_colissimo_info(tracknum)
|
||||
if info:
|
||||
date, libelle, site = info
|
||||
return ("Colissimo: \x02%s\x0F : \x02%s\x0F Dernière mise à jour le "
|
||||
"\x02%s\x0F au site \x02%s\x0F."
|
||||
% (tracknum, libelle, date, site))
|
||||
|
||||
|
||||
def handle_chronopost(tracknum):
|
||||
info = get_chronopost_info(tracknum)
|
||||
if info:
|
||||
date, libelle = info
|
||||
return ("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à "
|
||||
"jour \x02%s\x0F." % (tracknum, libelle, date))
|
||||
|
||||
|
||||
def handle_coliprive(tracknum):
|
||||
info = get_colisprive_info(tracknum)
|
||||
if info:
|
||||
return ("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (tracknum, info))
|
||||
|
||||
|
||||
def handle_fedex(tracknum):
|
||||
info = get_fedex_info(tracknum)
|
||||
if info:
|
||||
if info["displayActDeliveryDateTime"] != "":
|
||||
return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, delivered on: {displayActDeliveryDateTime}.".format(**info))
|
||||
elif info["statusLocationCity"] != "":
|
||||
return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: estimated delivery: {displayEstDeliveryDateTime}.".format(**info))
|
||||
else:
|
||||
return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, estimated delivery: {displayEstDeliveryDateTime}.".format(**info))
|
||||
|
||||
|
||||
def handle_dhl(tracknum):
|
||||
info = get_dhl_info(tracknum)
|
||||
if info:
|
||||
return "DHL {label} {id}: \x02{description}\x0F".format(**info)
|
||||
|
||||
|
||||
TRACKING_HANDLERS = {
|
||||
'laposte': handle_laposte,
|
||||
'postnl': handle_postnl,
|
||||
'colissimo': handle_colissimo,
|
||||
'chronopost': handle_chronopost,
|
||||
'coliprive': handle_coliprive,
|
||||
'tnt': handle_tnt,
|
||||
'fedex': handle_fedex,
|
||||
'dhl': handle_dhl,
|
||||
'usps': handle_usps,
|
||||
'ups': handle_ups,
|
||||
}
|
||||
|
||||
|
||||
# HOOKS ##############################################################
|
||||
|
||||
@hook.command("track",
|
||||
help="Track postage delivery",
|
||||
help_usage={
|
||||
"TRACKING_ID [...]": "Track the specified postage IDs on various tracking services."
|
||||
},
|
||||
keywords={
|
||||
"tracker=TRK": "Precise the tracker (default: all) among: " + ', '.join(TRACKING_HANDLERS)
|
||||
})
|
||||
def get_tracking_info(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Renseignez un identifiant d'envoi,")
|
||||
raise IMException("Renseignez un identifiant d'envoi.")
|
||||
|
||||
info = get_colisprive_info(msg.args[0])
|
||||
if info:
|
||||
return Response("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (msg.args[0], info), msg.channel)
|
||||
res = Response(channel=msg.channel, count=" (%d suivis supplémentaires)")
|
||||
|
||||
info = get_chronopost_info(msg.args[0])
|
||||
if info:
|
||||
date, libelle = info
|
||||
return Response("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (msg.args[0], libelle, date), msg.channel)
|
||||
if 'tracker' in msg.kwargs:
|
||||
if msg.kwargs['tracker'] in TRACKING_HANDLERS:
|
||||
trackers = {
|
||||
msg.kwargs['tracker']: TRACKING_HANDLERS[msg.kwargs['tracker']]
|
||||
}
|
||||
else:
|
||||
raise IMException("No tracker named \x02{tracker}\x0F, please use"
|
||||
" one of the following: \x02{trackers}\x0F"
|
||||
.format(tracker=msg.kwargs['tracker'],
|
||||
trackers=', '
|
||||
.join(TRACKING_HANDLERS.keys())))
|
||||
else:
|
||||
trackers = TRACKING_HANDLERS
|
||||
|
||||
info = get_colissimo_info(msg.args[0])
|
||||
if info:
|
||||
date, libelle, site = info
|
||||
return Response("Colissimo: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (msg.args[0], libelle, date, site), msg.channel)
|
||||
|
||||
info = get_laposte_info(msg.args[0])
|
||||
if info:
|
||||
poste_type, poste_id, poste_status, poste_location, poste_date = info
|
||||
return Response("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date), msg.channel)
|
||||
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
|
||||
|
||||
@hook("cmd_hook", "colisprive")
|
||||
def get_colisprive_tracking_info(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Renseignez un identifiant d'envoi,")
|
||||
info = get_colisprive_info(msg.args[0])
|
||||
if info:
|
||||
return Response("Colis: \x02%s\x0F : \x02%s\x0F." % (msg.args[0], info), msg.channel)
|
||||
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
|
||||
|
||||
@hook("cmd_hook", "chronopost")
|
||||
def get_chronopost_tracking_info(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Renseignez un identifiant d'envoi,")
|
||||
info = get_chronopost_info(msg.args[0])
|
||||
if info:
|
||||
date, libelle = info
|
||||
return Response("Colis: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (msg.args[0], libelle, date), msg.channel)
|
||||
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
|
||||
|
||||
@hook("cmd_hook", "colissimo")
|
||||
def get_colissimo_tracking_info(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Renseignez un identifiant d'envoi,")
|
||||
info = get_colissimo_info(msg.args[0])
|
||||
if info:
|
||||
date, libelle, site = info
|
||||
return Response("Colis: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (msg.args[0], libelle, date, site), msg.channel)
|
||||
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
|
||||
|
||||
@hook("cmd_hook", "laposte")
|
||||
def get_laposte_tracking_info(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Renseignez un identifiant d'envoi,")
|
||||
info = get_laposte_info(msg.args[0])
|
||||
if info:
|
||||
poste_type, poste_id, poste_status, poste_location, poste_date = info
|
||||
return Response("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date), msg.channel)
|
||||
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
|
||||
for tracknum in msg.args:
|
||||
for name, tracker in trackers.items():
|
||||
ret = tracker(tracknum)
|
||||
if ret:
|
||||
res.append_message(ret)
|
||||
break
|
||||
if not ret:
|
||||
res.append_message("L'identifiant \x02{id}\x0F semble incorrect,"
|
||||
" merci de vérifier son exactitude."
|
||||
.format(id=tracknum))
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Find synonyms"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 4.0
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
def help_full():
|
||||
return "!syno [LANG] <word>: give a list of synonyms for <word>."
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
global lang_binding
|
||||
|
||||
if not context.config or not context.config.hasAttribute("bighugelabskey"):
|
||||
if not context.config or not "bighugelabskey" in context.config:
|
||||
logger.error("You need a NigHugeLabs API key in order to have english "
|
||||
"theasorus. Add it to the module configuration file:\n"
|
||||
"<module name=\"syno\" bighugelabskey=\"XXXXXXXXXXXXXXXX\""
|
||||
|
|
@ -30,8 +26,10 @@ def load(context):
|
|||
lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word)
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def get_french_synos(word):
|
||||
url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1"))
|
||||
url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word)
|
||||
page = web.getURLContent(url)
|
||||
|
||||
best = list(); synos = list(); anton = list()
|
||||
|
|
@ -55,7 +53,7 @@ def get_french_synos(word):
|
|||
|
||||
|
||||
def get_english_synos(key, word):
|
||||
cnt = web.getJSON("http://words.bighugelabs.com/api/2/%s/%s/json" %
|
||||
cnt = web.getJSON("https://words.bighugelabs.com/api/2/%s/%s/json" %
|
||||
(quote(key), quote(word.encode("ISO-8859-1"))))
|
||||
|
||||
best = list(); synos = list(); anton = list()
|
||||
|
|
@ -72,24 +70,29 @@ def get_english_synos(key, word):
|
|||
lang_binding = { 'fr': get_french_synos }
|
||||
|
||||
|
||||
@hook("cmd_hook", "synonymes", data="synonymes")
|
||||
@hook("cmd_hook", "antonymes", data="antonymes")
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("synonymes", data="synonymes",
|
||||
help="give a list of synonyms",
|
||||
help_usage={"WORD": "give synonyms of the given WORD"},
|
||||
keywords={
|
||||
"lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding)
|
||||
})
|
||||
@hook.command("antonymes", data="antonymes",
|
||||
help="give a list of antonyms",
|
||||
help_usage={"WORD": "give antonyms of the given WORD"},
|
||||
keywords={
|
||||
"lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding)
|
||||
})
|
||||
def go(msg, what):
|
||||
if not len(msg.args):
|
||||
raise IRCException("de quel mot veux-tu connaître la liste des synonymes ?")
|
||||
raise IMException("de quel mot veux-tu connaître la liste des synonymes ?")
|
||||
|
||||
# Detect lang
|
||||
if msg.args[0] in lang_binding:
|
||||
func = lang_binding[msg.args[0]]
|
||||
word = ' '.join(msg.args[1:])
|
||||
else:
|
||||
func = lang_binding["fr"]
|
||||
word = ' '.join(msg.args)
|
||||
# TODO: depreciate usage without lang
|
||||
#raise IRCException("language %s is not handled yet." % msg.args[0])
|
||||
lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr"
|
||||
word = ' '.join(msg.args)
|
||||
|
||||
try:
|
||||
best, synos, anton = func(word)
|
||||
best, synos, anton = lang_binding[lang](word)
|
||||
except:
|
||||
best, synos, anton = (list(), list(), list())
|
||||
|
||||
|
|
@ -100,7 +103,7 @@ def go(msg, what):
|
|||
if len(synos) > 0: res.append_message(synos)
|
||||
return res
|
||||
else:
|
||||
raise IRCException("Aucun synonyme de %s n'a été trouvé" % word)
|
||||
raise IMException("Aucun synonyme de %s n'a été trouvé" % word)
|
||||
|
||||
elif what == "antonymes":
|
||||
if len(anton) > 0:
|
||||
|
|
@ -108,7 +111,7 @@ def go(msg, what):
|
|||
title="Antonymes de %s" % word)
|
||||
return res
|
||||
else:
|
||||
raise IRCException("Aucun antonyme de %s n'a été trouvé" % word)
|
||||
raise IMException("Aucun antonyme de %s n'a été trouvé" % word)
|
||||
|
||||
else:
|
||||
raise IRCException("WHAT?!")
|
||||
raise IMException("WHAT?!")
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
from datetime import datetime
|
||||
import urllib
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import human
|
||||
from nemubot.tools.web import getJSON
|
||||
|
||||
nemubotversion = 4.0
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
URL_TPBAPI = None
|
||||
|
||||
def load(context):
|
||||
if not context.config or not context.config.hasAttribute("url"):
|
||||
if not context.config or "url" not in context.config:
|
||||
raise ImportError("You need a TPB API in order to use the !tpb feature"
|
||||
". Add it to the module configuration file:\n<module"
|
||||
"name=\"tpb\" url=\"http://tpbapi.org/\" />\nSample "
|
||||
|
|
@ -22,10 +22,10 @@ def load(context):
|
|||
global URL_TPBAPI
|
||||
URL_TPBAPI = context.config["url"]
|
||||
|
||||
@hook("cmd_hook", "tpb")
|
||||
@hook.command("tpb")
|
||||
def cmd_tpb(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("indicate an item to search!")
|
||||
raise IMException("indicate an item to search!")
|
||||
|
||||
torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.args)))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Translation module"""
|
||||
|
||||
import re
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 4.0
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from more import Response
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it",
|
||||
"ja", "ko", "pl", "pt", "ro", "es", "tr"]
|
||||
URL = "http://api.wordreference.com/0.8/%s/json/%%s%%s/%%s"
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
def load(context):
|
||||
if not context.config or not context.config.hasAttribute("wrapikey"):
|
||||
if not context.config or "wrapikey" not in context.config:
|
||||
raise ImportError("You need a WordReference API key in order to use "
|
||||
"this module. Add it to the module configuration "
|
||||
"file:\n<module name=\"translate\" wrapikey=\"XXXXX\""
|
||||
|
|
@ -29,57 +31,7 @@ def load(context):
|
|||
URL = URL % context.config["wrapikey"]
|
||||
|
||||
|
||||
def help_full():
|
||||
return "!translate [lang] <term>[ <term>[...]]: Found translation of <term> from/to english to/from <lang>. Data © WordReference.com"
|
||||
|
||||
|
||||
@hook("cmd_hook", "translate")
|
||||
def cmd_translate(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("which word would you translate?")
|
||||
|
||||
if len(msg.args) > 2 and msg.args[0] in LANG and msg.args[1] in LANG:
|
||||
if msg.args[0] != "en" and msg.args[1] != "en":
|
||||
raise IRCException("sorry, I can only translate to or from english")
|
||||
langFrom = msg.args[0]
|
||||
langTo = msg.args[1]
|
||||
term = ' '.join(msg.args[2:])
|
||||
elif len(msg.args) > 1 and msg.args[0] in LANG:
|
||||
langFrom = msg.args[0]
|
||||
if langFrom == "en":
|
||||
langTo = "fr"
|
||||
else:
|
||||
langTo = "en"
|
||||
term = ' '.join(msg.args[1:])
|
||||
else:
|
||||
langFrom = "en"
|
||||
langTo = "fr"
|
||||
term = ' '.join(msg.args)
|
||||
|
||||
wres = web.getJSON(URL % (langFrom, langTo, quote(term)))
|
||||
|
||||
if "Error" in wres:
|
||||
raise IRCException(wres["Note"])
|
||||
|
||||
else:
|
||||
res = Response(channel=msg.channel,
|
||||
count=" (%d more meanings)",
|
||||
nomore="No more translation")
|
||||
for k in sorted(wres.keys()):
|
||||
t = wres[k]
|
||||
if len(k) > 4 and k[:4] == "term":
|
||||
if "Entries" in t:
|
||||
ent = t["Entries"]
|
||||
else:
|
||||
ent = t["PrincipalTranslations"]
|
||||
|
||||
for i in sorted(ent.keys()):
|
||||
res.append_message("Translation of %s%s: %s" % (
|
||||
ent[i]["OriginalTerm"]["term"],
|
||||
meaning(ent[i]["OriginalTerm"]),
|
||||
extract_traslation(ent[i])))
|
||||
return res
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def meaning(entry):
|
||||
ret = list()
|
||||
|
|
@ -101,3 +53,59 @@ def extract_traslation(entry):
|
|||
if "Note" in entry and entry["Note"]:
|
||||
ret.append("note: %s" % entry["Note"])
|
||||
return ", ".join(ret)
|
||||
|
||||
|
||||
def translate(term, langFrom="en", langTo="fr"):
|
||||
wres = web.getJSON(URL % (langFrom, langTo, quote(term)))
|
||||
|
||||
if "Error" in wres:
|
||||
raise IMException(wres["Note"])
|
||||
|
||||
else:
|
||||
for k in sorted(wres.keys()):
|
||||
t = wres[k]
|
||||
if len(k) > 4 and k[:4] == "term":
|
||||
if "Entries" in t:
|
||||
ent = t["Entries"]
|
||||
else:
|
||||
ent = t["PrincipalTranslations"]
|
||||
|
||||
for i in sorted(ent.keys()):
|
||||
yield "Translation of %s%s: %s" % (
|
||||
ent[i]["OriginalTerm"]["term"],
|
||||
meaning(ent[i]["OriginalTerm"]),
|
||||
extract_traslation(ent[i]))
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("translate",
|
||||
help="Word translation using WordReference.com",
|
||||
help_usage={
|
||||
"TERM": "Found translation of TERM from/to english to/from <lang>."
|
||||
},
|
||||
keywords={
|
||||
"from=LANG": "language of the term you asked for translation between: en, " + ", ".join(LANG),
|
||||
"to=LANG": "language of the translated terms between: en, " + ", ".join(LANG),
|
||||
})
|
||||
def cmd_translate(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("which word would you translate?")
|
||||
|
||||
langFrom = msg.kwargs["from"] if "from" in msg.kwargs else "en"
|
||||
if "to" in msg.kwargs:
|
||||
langTo = msg.kwargs["to"]
|
||||
else:
|
||||
langTo = "fr" if langFrom == "en" else "en"
|
||||
|
||||
if langFrom not in LANG or langTo not in LANG:
|
||||
raise IMException("sorry, I can only translate to or from: " + ", ".join(LANG))
|
||||
if langFrom != "en" and langTo != "en":
|
||||
raise IMException("sorry, I can only translate to or from english")
|
||||
|
||||
res = Response(channel=msg.channel,
|
||||
count=" (%d more meanings)",
|
||||
nomore="No more translation")
|
||||
for t in translate(" ".join(msg.args), langFrom=langFrom, langTo=langTo):
|
||||
res.append_message(t)
|
||||
return res
|
||||
|
|
|
|||
37
modules/urbandict.py
Normal file
37
modules/urbandict.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Search definition from urbandictionnary"""
|
||||
|
||||
# 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 search(terms):
|
||||
return web.getJSON(
|
||||
"https://api.urbandictionary.com/v0/define?term=%s"
|
||||
% quote(' '.join(terms)))
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("urbandictionnary")
|
||||
def udsearch(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("Indicate a term to search")
|
||||
|
||||
s = search(msg.args)
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more results",
|
||||
count=" (%d more definitions)")
|
||||
|
||||
for i in s["list"]:
|
||||
res.append_message(i["definition"].replace("\n", " "),
|
||||
title=i["word"])
|
||||
|
||||
return res
|
||||
173
modules/urlreducer.py
Normal file
173
modules/urlreducer.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
"""URL reducer module"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.message import Text
|
||||
from nemubot.tools import web
|
||||
|
||||
|
||||
# MODULE FUNCTIONS ####################################################
|
||||
|
||||
def default_reducer(url, data):
|
||||
snd_url = url + quote(data, "/:%@&=?")
|
||||
return web.getURLContent(snd_url)
|
||||
|
||||
|
||||
def ycc_reducer(url, data):
|
||||
return "https://ycc.fr/%s" % default_reducer(url, data)
|
||||
|
||||
def lstu_reducer(url, data):
|
||||
json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data),
|
||||
header={"Content-Type": "application/x-www-form-urlencoded"}))
|
||||
if 'short' in json_data:
|
||||
return json_data['short']
|
||||
elif 'msg' in json_data:
|
||||
raise IMException("Error: %s" % json_data['msg'])
|
||||
else:
|
||||
IMException("An error occured while shortening %s." % data)
|
||||
|
||||
# MODULE VARIABLES ####################################################
|
||||
|
||||
PROVIDERS = {
|
||||
"tinyurl": (default_reducer, "https://tinyurl.com/api-create.php?url="),
|
||||
"ycc": (ycc_reducer, "https://ycc.fr/redirection/create/"),
|
||||
"framalink": (lstu_reducer, "https://frama.link/a?format=json"),
|
||||
"huitre": (lstu_reducer, "https://huit.re/a?format=json"),
|
||||
"lstu": (lstu_reducer, "https://lstu.fr/a?format=json"),
|
||||
}
|
||||
DEFAULT_PROVIDER = "framalink"
|
||||
|
||||
PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, url in PROVIDERS.values()]
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
|
||||
def load(context):
|
||||
global DEFAULT_PROVIDER
|
||||
|
||||
if "provider" in context.config:
|
||||
if context.config["provider"] == "custom":
|
||||
PROVIDERS["custom"] = context.config["provider_url"]
|
||||
DEFAULT_PROVIDER = context.config["provider"]
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def reduce_inline(txt, provider=None):
|
||||
for url in re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", txt):
|
||||
txt = txt.replace(url, reduce(url, provider))
|
||||
return txt
|
||||
|
||||
|
||||
def reduce(url, provider=None):
|
||||
"""Ask the url shortner website to reduce given URL
|
||||
|
||||
Argument:
|
||||
url -- the URL to reduce
|
||||
"""
|
||||
if provider is None:
|
||||
provider = DEFAULT_PROVIDER
|
||||
return PROVIDERS[provider][0](PROVIDERS[provider][1], url)
|
||||
|
||||
|
||||
def gen_response(res, msg, srv):
|
||||
if res is None:
|
||||
raise IMException("bad URL : %s" % srv)
|
||||
else:
|
||||
return Text("URL for %s: %s" % (srv, res), server=None,
|
||||
to=msg.to_response)
|
||||
|
||||
|
||||
## URL stack
|
||||
|
||||
LAST_URLS = dict()
|
||||
|
||||
|
||||
@hook.message()
|
||||
def parselisten(msg):
|
||||
global LAST_URLS
|
||||
if hasattr(msg, "message") and isinstance(msg.message, str):
|
||||
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)",
|
||||
msg.message)
|
||||
for url in urls:
|
||||
o = urlparse(web._getNormalizedURL(url), "http")
|
||||
|
||||
# Skip short URLs
|
||||
if (o.netloc == "" or o.netloc in PROVIDERS or
|
||||
len(o.netloc) + len(o.path) < 17):
|
||||
continue
|
||||
|
||||
for recv in msg.to:
|
||||
if recv not in LAST_URLS:
|
||||
LAST_URLS[recv] = list()
|
||||
LAST_URLS[recv].append(url)
|
||||
|
||||
|
||||
@hook.post()
|
||||
def parseresponse(msg):
|
||||
global LAST_URLS
|
||||
if hasattr(msg, "text") and isinstance(msg.text, str):
|
||||
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)",
|
||||
msg.text)
|
||||
for url in urls:
|
||||
o = urlparse(web._getNormalizedURL(url), "http")
|
||||
|
||||
# Skip short URLs
|
||||
if (o.netloc == "" or o.netloc in PROVIDERS or
|
||||
len(o.netloc) + len(o.path) < 17):
|
||||
continue
|
||||
|
||||
for recv in msg.to:
|
||||
if recv not in LAST_URLS:
|
||||
LAST_URLS[recv] = list()
|
||||
LAST_URLS[recv].append(url)
|
||||
return msg
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("framalink",
|
||||
help="Reduce any long URL",
|
||||
help_usage={
|
||||
None: "Reduce the last URL said on the channel",
|
||||
"URL [URL ...]": "Reduce the given URL(s)"
|
||||
},
|
||||
keywords={
|
||||
"provider=SMTH": "Change the service provider used (by default: %s) among %s" % (DEFAULT_PROVIDER, ", ".join(PROVIDERS.keys()))
|
||||
})
|
||||
def cmd_reduceurl(msg):
|
||||
minify = list()
|
||||
|
||||
if not len(msg.args):
|
||||
global LAST_URLS
|
||||
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
|
||||
minify.append(LAST_URLS[msg.channel].pop())
|
||||
else:
|
||||
raise IMException("I have no more URL to reduce.")
|
||||
|
||||
if len(msg.args) > 4:
|
||||
raise IMException("I cannot reduce that many URLs at once.")
|
||||
else:
|
||||
minify += msg.args
|
||||
|
||||
if 'provider' in msg.kwargs and msg.kwargs['provider'] in PROVIDERS:
|
||||
provider = msg.kwargs['provider']
|
||||
else:
|
||||
provider = DEFAULT_PROVIDER
|
||||
|
||||
res = list()
|
||||
for url in minify:
|
||||
o = urlparse(web.getNormalizedURL(url), "http")
|
||||
minief_url = reduce(url, provider)
|
||||
if o.netloc == "":
|
||||
res.append(gen_response(minief_url, msg, o.scheme))
|
||||
else:
|
||||
res.append(gen_response(minief_url, msg, o.netloc))
|
||||
return res
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""Gets information about velib stations"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 4.0
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from more import Response
|
||||
|
||||
# LOADING #############################################################
|
||||
|
||||
URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s
|
||||
|
||||
def load(context):
|
||||
global URL_API
|
||||
if not context.config or not context.config.hasAttribute("url"):
|
||||
if not context.config or "url" not in context.config:
|
||||
raise ImportError("Please provide url attribute in the module configuration")
|
||||
URL_API = context.config["url"]
|
||||
context.data.setIndex("name", "station")
|
||||
|
|
@ -29,25 +29,14 @@ def load(context):
|
|||
# context.add_event(evt)
|
||||
|
||||
|
||||
def help_full():
|
||||
return ("!velib /number/ ...: gives available bikes and slots at "
|
||||
"the station /number/.")
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def station_status(station):
|
||||
"""Gets available and free status of a given station"""
|
||||
response = web.getXML(URL_API % station)
|
||||
if response is not None:
|
||||
available = response.getNode("available").getContent()
|
||||
if available is not None and len(available) > 0:
|
||||
available = int(available)
|
||||
else:
|
||||
available = 0
|
||||
free = response.getNode("free").getContent()
|
||||
if free is not None and len(free) > 0:
|
||||
free = int(free)
|
||||
else:
|
||||
free = 0
|
||||
available = int(response.getElementsByTagName("available")[0].firstChild.nodeValue)
|
||||
free = int(response.getElementsByTagName("free")[0].firstChild.nodeValue)
|
||||
return (available, free)
|
||||
else:
|
||||
return (None, None)
|
||||
|
|
@ -69,27 +58,30 @@ def print_station_status(msg, station):
|
|||
"""Send message with information about the given station"""
|
||||
(available, free) = station_status(station)
|
||||
if available is not None and free is not None:
|
||||
return Response("à la station %s : %d vélib et %d points d'attache"
|
||||
return Response("À la station %s : %d vélib et %d points d'attache"
|
||||
" disponibles." % (station, available, free),
|
||||
channel=msg.channel, nick=msg.nick)
|
||||
raise IRCException("station %s inconnue." % station)
|
||||
channel=msg.channel)
|
||||
raise IMException("station %s inconnue." % station)
|
||||
|
||||
|
||||
@hook("cmd_hook", "velib")
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("velib",
|
||||
help="gives available bikes and slots at the given station",
|
||||
help_usage={
|
||||
"STATION_ID": "gives available bikes and slots at the station STATION_ID"
|
||||
})
|
||||
def ask_stations(msg):
|
||||
"""Hook entry from !velib"""
|
||||
if len(msg.args) > 4:
|
||||
raise IRCException("demande-moi moins de stations à la fois.")
|
||||
raise IMException("demande-moi moins de stations à la fois.")
|
||||
elif not len(msg.args):
|
||||
raise IMException("pour quelle station ?")
|
||||
|
||||
elif len(msg.args):
|
||||
for station in msg.args:
|
||||
if re.match("^[0-9]{4,5}$", station):
|
||||
return print_station_status(msg, station)
|
||||
elif station in context.data.index:
|
||||
return print_station_status(msg,
|
||||
context.data.index[station]["number"])
|
||||
else:
|
||||
raise IRCException("numéro de station invalide.")
|
||||
|
||||
else:
|
||||
raise IRCException("pour quelle station ?")
|
||||
for station in msg.args:
|
||||
if re.match("^[0-9]{4,5}$", station):
|
||||
return print_station_status(msg, station)
|
||||
elif station in context.data.index:
|
||||
return print_station_status(msg,
|
||||
context.data.index[station]["number"])
|
||||
else:
|
||||
raise IMException("numéro de station invalide.")
|
||||
|
|
|
|||
100
modules/virtualradar.py
Normal file
100
modules/virtualradar.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""Retrieve flight information from VirtualRadar APIs"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
import time
|
||||
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
from nemubot.module.more import Response
|
||||
from nemubot.module import mapquest
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
URL_API = "https://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s"
|
||||
|
||||
SPEED_TYPES = {
|
||||
0: 'Ground speed',
|
||||
1: 'Ground speed reversing',
|
||||
2: 'Indicated air speed',
|
||||
3: 'True air speed'}
|
||||
|
||||
WTC_CAT = {
|
||||
0: 'None',
|
||||
1: 'Light',
|
||||
2: 'Medium',
|
||||
3: 'Heavy'
|
||||
}
|
||||
|
||||
SPECIES = {
|
||||
1: 'Land plane',
|
||||
2: 'Sea plane',
|
||||
3: 'Amphibian',
|
||||
4: 'Helicopter',
|
||||
5: 'Gyrocopter',
|
||||
6: 'Tiltwing',
|
||||
7: 'Ground vehicle',
|
||||
8: 'Tower'}
|
||||
|
||||
HANDLER_TABLE = {
|
||||
'From': lambda x: 'From: \x02%s\x0F' % x,
|
||||
'To': lambda x: 'To: \x02%s\x0F' % x,
|
||||
'Op': lambda x: 'Airline: \x02%s\x0F' % x,
|
||||
'Mdl': lambda x: 'Model: \x02%s\x0F' % x,
|
||||
'Call': lambda x: 'Flight: \x02%s\x0F' % x,
|
||||
'PosTime': lambda x: 'Last update: \x02%s\x0F' % (time.ctime(int(x)/1000)),
|
||||
'Alt': lambda x: 'Altitude: \x02%s\x0F ft' % x,
|
||||
'Spd': lambda x: 'Speed: \x02%s\x0F kn' % x,
|
||||
'SpdTyp': lambda x: 'Speed type: \x02%s\x0F' % SPEED_TYPES[x] if x in SPEED_TYPES else None,
|
||||
'Engines': lambda x: 'Engines: \x02%s\x0F' % x,
|
||||
'Gnd': lambda x: 'On the ground' if x else None,
|
||||
'Mil': lambda x: 'Military aicraft' if x else None,
|
||||
'Species': lambda x: 'Aircraft species: \x02%s\x0F' % SPECIES[x] if x in SPECIES else None,
|
||||
'WTC': lambda x: 'Turbulence level: \x02%s\x0F' % WTC_CAT[x] if x in WTC_CAT else None,
|
||||
}
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
def virtual_radar(flight_call):
|
||||
obj = web.getJSON(URL_API % quote(flight_call))
|
||||
|
||||
if "acList" in obj:
|
||||
for flight in obj["acList"]:
|
||||
yield flight
|
||||
|
||||
def flight_info(flight):
|
||||
for prop in HANDLER_TABLE:
|
||||
if prop in flight:
|
||||
yield HANDLER_TABLE[prop](flight[prop])
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("flight",
|
||||
help="Get flight information",
|
||||
help_usage={ "FLIGHT": "Get information on FLIGHT" })
|
||||
def cmd_flight(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("please indicate a flight")
|
||||
|
||||
res = Response(channel=msg.channel, nick=msg.frm,
|
||||
nomore="No more flights", count=" (%s more flights)")
|
||||
|
||||
for param in msg.args:
|
||||
for flight in virtual_radar(param):
|
||||
if 'Lat' in flight and 'Long' in flight:
|
||||
loc = None
|
||||
for location in mapquest.geocode('{Lat},{Long}'.format(**flight)):
|
||||
loc = location
|
||||
break
|
||||
if loc:
|
||||
res.append_message('\x02{0}\x0F: Position: \x02{1}\x0F, {2}'.format(flight['Call'], \
|
||||
mapquest.where(loc), \
|
||||
', '.join(filter(None, flight_info(flight)))))
|
||||
continue
|
||||
res.append_message('\x02{0}\x0F: {1}'.format(flight['Call'], \
|
||||
', '.join(filter(None, flight_info(flight)))))
|
||||
return res
|
||||
|
|
@ -1,82 +1,81 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""The weather module"""
|
||||
"""The weather module. Powered by Dark Sky <https://darksky.net/poweredby/>"""
|
||||
|
||||
import datetime
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
import mapquest
|
||||
from nemubot.module import mapquest
|
||||
|
||||
nemubotversion = 4.0
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
URL_DSAPI = "https://api.forecast.io/forecast/%s/%%s,%%s"
|
||||
URL_DSAPI = "https://api.darksky.net/forecast/%s/%%s,%%s?lang=%%s&units=%%s"
|
||||
|
||||
UNITS = {
|
||||
"ca": {
|
||||
"temperature": "°C",
|
||||
"distance": "km",
|
||||
"precipIntensity": "mm/h",
|
||||
"precip": "cm",
|
||||
"speed": "km/h",
|
||||
"pressure": "hPa",
|
||||
},
|
||||
"uk2": {
|
||||
"temperature": "°C",
|
||||
"distance": "mi",
|
||||
"precipIntensity": "mm/h",
|
||||
"precip": "cm",
|
||||
"speed": "mi/h",
|
||||
"pressure": "hPa",
|
||||
},
|
||||
"us": {
|
||||
"temperature": "°F",
|
||||
"distance": "mi",
|
||||
"precipIntensity": "in/h",
|
||||
"precip": "in",
|
||||
"speed": "mi/h",
|
||||
"pressure": "mbar",
|
||||
},
|
||||
"si": {
|
||||
"temperature": "°C",
|
||||
"distance": "km",
|
||||
"precipIntensity": "mm/h",
|
||||
"precip": "cm",
|
||||
"speed": "m/s",
|
||||
"pressure": "hPa",
|
||||
},
|
||||
}
|
||||
|
||||
def load(context):
|
||||
if not context.config or not context.config.hasAttribute("darkskyapikey"):
|
||||
if not context.config or "darkskyapikey" not in context.config:
|
||||
raise ImportError("You need a Dark-Sky API key in order to use this "
|
||||
"module. Add it to the module configuration file:\n"
|
||||
"<module name=\"weather\" darkskyapikey=\"XXX\" />\n"
|
||||
"Register at http://developer.forecast.io/")
|
||||
"Register at https://developer.forecast.io/")
|
||||
context.data.setIndex("name", "city")
|
||||
global URL_DSAPI
|
||||
URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"]
|
||||
|
||||
|
||||
def help_full ():
|
||||
return "!weather /city/: Display the current weather in /city/."
|
||||
def format_wth(wth, flags):
|
||||
units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"]
|
||||
return ("{temperature} {units[temperature]} {summary}; precipitation ({precipProbability:.0%} chance) intensity: {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU"
|
||||
.format(units=units, **wth)
|
||||
)
|
||||
|
||||
|
||||
def fahrenheit2celsius(temp):
|
||||
return int((temp - 32) * 50/9)/10
|
||||
|
||||
|
||||
def mph2kmph(speed):
|
||||
return int(speed * 160.9344)/100
|
||||
|
||||
|
||||
def inh2mmh(size):
|
||||
return int(size * 254)/10
|
||||
|
||||
|
||||
def format_wth(wth):
|
||||
return ("%s °C %s; precipitation (%s %% chance) intensity: %s mm/h; relative humidity: %s %%; wind speed: %s km/h %s°; cloud coverage: %s %%; pressure: %s hPa; ozone: %s DU" %
|
||||
(
|
||||
fahrenheit2celsius(wth["temperature"]),
|
||||
wth["summary"],
|
||||
int(wth["precipProbability"] * 100),
|
||||
inh2mmh(wth["precipIntensity"]),
|
||||
int(wth["humidity"] * 100),
|
||||
mph2kmph(wth["windSpeed"]),
|
||||
wth["windBearing"],
|
||||
int(wth["cloudCover"] * 100),
|
||||
int(wth["pressure"]),
|
||||
int(wth["ozone"])
|
||||
))
|
||||
|
||||
|
||||
def format_forecast_daily(wth):
|
||||
return ("%s; between %s-%s °C; precipitation (%s %% chance) intensity: maximum %s mm/h; relative humidity: %s %%; wind speed: %s km/h %s°; cloud coverage: %s %%; pressure: %s hPa; ozone: %s DU" %
|
||||
(
|
||||
wth["summary"],
|
||||
fahrenheit2celsius(wth["temperatureMin"]), fahrenheit2celsius(wth["temperatureMax"]),
|
||||
int(wth["precipProbability"] * 100),
|
||||
inh2mmh(wth["precipIntensityMax"]),
|
||||
int(wth["humidity"] * 100),
|
||||
mph2kmph(wth["windSpeed"]),
|
||||
wth["windBearing"],
|
||||
int(wth["cloudCover"] * 100),
|
||||
int(wth["pressure"]),
|
||||
int(wth["ozone"])
|
||||
))
|
||||
def format_forecast_daily(wth, flags):
|
||||
units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"]
|
||||
print(units)
|
||||
return ("{summary}; between {temperatureMin}-{temperatureMax} {units[temperature]}; precipitation ({precipProbability:.0%} chance) intensity: maximum {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU".format(units=units, **wth))
|
||||
|
||||
|
||||
def format_timestamp(timestamp, tzname, tzoffset, format="%c"):
|
||||
|
|
@ -121,60 +120,82 @@ def treat_coord(msg):
|
|||
coords.append(geocode[0]["latLng"]["lng"])
|
||||
return mapquest.where(geocode[0]), coords, specific
|
||||
|
||||
raise IRCException("Je ne sais pas où se trouve %s." % city)
|
||||
raise IMException("Je ne sais pas où se trouve %s." % city)
|
||||
|
||||
else:
|
||||
raise IRCException("indique-moi un nom de ville ou des coordonnées.")
|
||||
raise IMException("indique-moi un nom de ville ou des coordonnées.")
|
||||
|
||||
|
||||
def get_json_weather(coords):
|
||||
wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1])))
|
||||
def get_json_weather(coords, lang="en", units="ca"):
|
||||
wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units))
|
||||
|
||||
# First read flags
|
||||
if wth is None or "darksky-unavailable" in wth["flags"]:
|
||||
raise IRCException("The given location is supported but a temporary error (such as a radar station being down for maintenace) made data unavailable.")
|
||||
raise IMException("The given location is supported but a temporary error (such as a radar station being down for maintenace) made data unavailable.")
|
||||
|
||||
return wth
|
||||
|
||||
|
||||
@hook("cmd_hook", "coordinates")
|
||||
@hook.command("coordinates")
|
||||
def cmd_coordinates(msg):
|
||||
if len(msg.args) < 1:
|
||||
raise IRCException("indique-moi un nom de ville.")
|
||||
raise IMException("indique-moi un nom de ville.")
|
||||
|
||||
j = msg.args[0].lower()
|
||||
if j not in context.data.index:
|
||||
raise IRCException("%s n'est pas une ville connue" % msg.args[0])
|
||||
raise IMException("%s n'est pas une ville connue" % msg.args[0])
|
||||
|
||||
coords = context.data.index[j]
|
||||
return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel)
|
||||
|
||||
|
||||
@hook("cmd_hook", "alert")
|
||||
@hook.command("alert",
|
||||
keywords={
|
||||
"lang=LANG": "change the output language of weather sumarry; default: en",
|
||||
"units=UNITS": "return weather conditions in the requested units; default: ca",
|
||||
})
|
||||
def cmd_alert(msg):
|
||||
loc, coords, specific = treat_coord(msg)
|
||||
wth = get_json_weather(coords)
|
||||
wth = get_json_weather(coords,
|
||||
lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
|
||||
units=msg.kwargs["units"] if "units" in msg.kwargs else "ca")
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)")
|
||||
|
||||
if "alerts" in wth:
|
||||
for alert in wth["alerts"]:
|
||||
res.append_message("\x03\x02%s\x03\x02 (see %s expire on %s): %s" % (alert["title"], alert["uri"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]), alert["description"].replace("\n", " ")))
|
||||
if "expires" in alert:
|
||||
res.append_message("\x03\x02%s\x03\x02 (see %s expire on %s): %s" % (alert["title"], alert["uri"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]), alert["description"].replace("\n", " ")))
|
||||
else:
|
||||
res.append_message("\x03\x02%s\x03\x02 (see %s): %s" % (alert["title"], alert["uri"], alert["description"].replace("\n", " ")))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@hook("cmd_hook", "météo")
|
||||
@hook.command("météo",
|
||||
help="Display current weather and previsions",
|
||||
help_usage={
|
||||
"CITY": "Display the current weather and previsions in CITY",
|
||||
},
|
||||
keywords={
|
||||
"lang=LANG": "change the output language of weather sumarry; default: en",
|
||||
"units=UNITS": "return weather conditions in the requested units; default: ca",
|
||||
})
|
||||
def cmd_weather(msg):
|
||||
loc, coords, specific = treat_coord(msg)
|
||||
wth = get_json_weather(coords)
|
||||
wth = get_json_weather(coords,
|
||||
lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
|
||||
units=msg.kwargs["units"] if "units" in msg.kwargs else "ca")
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more weather information")
|
||||
|
||||
if "alerts" in wth:
|
||||
alert_msgs = list()
|
||||
for alert in wth["alerts"]:
|
||||
alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"])))
|
||||
if "expires" in alert:
|
||||
alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"])))
|
||||
else:
|
||||
alert_msgs.append("\x03\x02%s\x03\x02" % (alert["title"]))
|
||||
res.append_message("\x03\x16\x03\x02/!\\\x03\x02 Alert%s:\x03\x16 " % ("s" if len(alert_msgs) > 1 else "") + ", ".join(alert_msgs))
|
||||
|
||||
if specific is not None:
|
||||
|
|
@ -186,17 +207,17 @@ def cmd_weather(msg):
|
|||
|
||||
if gr.group(2).lower() == "h" and gr1 < len(wth["hourly"]["data"]):
|
||||
hour = wth["hourly"]["data"][gr1]
|
||||
res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour)))
|
||||
res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour, wth["flags"])))
|
||||
|
||||
elif gr.group(2).lower() == "d" and gr1 < len(wth["daily"]["data"]):
|
||||
day = wth["daily"]["data"][gr1]
|
||||
res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day)))
|
||||
res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day, wth["flags"])))
|
||||
|
||||
else:
|
||||
res.append_message("I don't understand %s or information is not available" % specific)
|
||||
|
||||
else:
|
||||
res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"]))
|
||||
res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"], wth["flags"]))
|
||||
|
||||
nextres = "\x03\x02Today:\x03\x02 %s " % wth["daily"]["data"][0]["summary"]
|
||||
if "minutely" in wth:
|
||||
|
|
@ -206,11 +227,11 @@ def cmd_weather(msg):
|
|||
|
||||
for hour in wth["hourly"]["data"][1:4]:
|
||||
res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'),
|
||||
format_wth(hour)))
|
||||
format_wth(hour, wth["flags"])))
|
||||
|
||||
for day in wth["daily"]["data"][1:]:
|
||||
res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'),
|
||||
format_forecast_daily(day)))
|
||||
format_forecast_daily(day, wth["flags"])))
|
||||
|
||||
return res
|
||||
|
||||
|
|
@ -218,9 +239,9 @@ def cmd_weather(msg):
|
|||
gps_ask = re.compile(r"^\s*(?P<city>.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]*)\s+[aà])\s*(?P<lat>-?[0-9]+(?:[,.][0-9]+))[^0-9.](?P<long>-?[0-9]+(?:[,.][0-9]+))\s*$", re.IGNORECASE)
|
||||
|
||||
|
||||
@hook("ask_default")
|
||||
@hook.ask()
|
||||
def parseask(msg):
|
||||
res = gps_ask.match(msg.text)
|
||||
res = gps_ask.match(msg.message)
|
||||
if res is not None:
|
||||
city_name = res.group("city").lower()
|
||||
gps_lat = res.group("lat").replace(",", ".")
|
||||
|
|
@ -237,4 +258,4 @@ def parseask(msg):
|
|||
context.data.addChild(ms)
|
||||
context.save()
|
||||
return Response("ok, j'ai bien noté les coordonnées de %s" % res.group("city"),
|
||||
msg.channel, msg.nick)
|
||||
msg.channel, msg.frm)
|
||||
|
|
|
|||
125
modules/whois.py
125
modules/whois.py
|
|
@ -1,55 +1,79 @@
|
|||
# coding=utf-8
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from networking.page import headers
|
||||
from nemubot.module.more import Response
|
||||
from nemubot.module.networking.page import headers
|
||||
|
||||
PASSWD_FILE = None
|
||||
# You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/?limit=10000' > users.json
|
||||
APIEXTRACT_FILE = None
|
||||
|
||||
def load(context):
|
||||
global PASSWD_FILE
|
||||
if not context.config or not context.config.hasAttribute("passwd"):
|
||||
if not context.config or "passwd" not in context.config:
|
||||
print("No passwd file given")
|
||||
else:
|
||||
PASSWD_FILE = context.config["passwd"]
|
||||
print("passwd file loaded:", PASSWD_FILE)
|
||||
|
||||
global APIEXTRACT_FILE
|
||||
if not context.config or "apiextract" not in context.config:
|
||||
print("No passwd file given")
|
||||
else:
|
||||
APIEXTRACT_FILE = context.config["apiextract"]
|
||||
print("JSON users file loaded:", APIEXTRACT_FILE)
|
||||
|
||||
if PASSWD_FILE is None and APIEXTRACT_FILE is None:
|
||||
return None
|
||||
PASSWD_FILE = context.config["passwd"]
|
||||
|
||||
if not context.data.hasNode("aliases"):
|
||||
context.data.addChild(ModuleState("aliases"))
|
||||
context.data.getNode("aliases").setIndex("from", "alias")
|
||||
|
||||
if not context.data.hasNode("pics"):
|
||||
context.data.addChild(ModuleState("pics"))
|
||||
context.data.getNode("pics").setIndex("login", "pict")
|
||||
|
||||
import nemubot.hooks
|
||||
context.add_hook("cmd_hook",
|
||||
nemubot.hooks.Message(cmd_whois, "whois"))
|
||||
context.add_hook(nemubot.hooks.Command(cmd_whois, "whois", keywords={"lookup": "Perform a lookup of the begining of the login instead of an exact search."}),
|
||||
"in","Command")
|
||||
|
||||
class Login:
|
||||
|
||||
def __init__(self, line):
|
||||
s = line.split(":")
|
||||
self.login = s[0]
|
||||
self.uid = s[2]
|
||||
self.gid = s[3]
|
||||
self.cn = s[4]
|
||||
self.home = s[5]
|
||||
def __init__(self, line=None, login=None, uidNumber=None, firstname=None, lastname=None, promo=None, **kwargs):
|
||||
if line is not None:
|
||||
s = line.split(":")
|
||||
self.login = s[0]
|
||||
self.uid = s[2]
|
||||
self.gid = s[3]
|
||||
self.cn = s[4]
|
||||
self.home = s[5]
|
||||
else:
|
||||
self.login = login
|
||||
self.uid = uidNumber
|
||||
self.promo = promo
|
||||
self.cn = firstname + " " + lastname
|
||||
try:
|
||||
self.gid = "epita" + str(int(promo))
|
||||
except:
|
||||
self.gid = promo
|
||||
|
||||
def get_promo(self):
|
||||
return self.home.split("/")[2].replace("_", " ")
|
||||
if hasattr(self, "promo"):
|
||||
return self.promo
|
||||
if hasattr(self, "home"):
|
||||
try:
|
||||
return self.home.split("/")[2].replace("_", " ")
|
||||
except:
|
||||
return self.gid
|
||||
|
||||
def get_photo(self):
|
||||
if self.login in context.data.getNode("pics").index:
|
||||
return context.data.getNode("pics").index[self.login]["url"]
|
||||
for url in [ "https://static.acu.epita.fr/photos/%s", "https://static.acu.epita.fr/photos/%s/%%s" % self.gid, "https://intra-bocal.epitech.eu/trombi/%s.jpg", "http://whois.23.tf/p/%s/%%s.jpg" % self.gid ]:
|
||||
for url in [ "https://photos.cri.epita.fr/%s", "https://intra-bocal.epitech.eu/trombi/%s.jpg" ]:
|
||||
url = url % self.login
|
||||
try:
|
||||
_, status, _, _ = headers(url)
|
||||
|
|
@ -60,38 +84,53 @@ class Login:
|
|||
return None
|
||||
|
||||
|
||||
def found_login(login):
|
||||
def login_lookup(login, search=False):
|
||||
if login in context.data.getNode("aliases").index:
|
||||
login = context.data.getNode("aliases").index[login]["to"]
|
||||
|
||||
login_ = login + ":"
|
||||
if APIEXTRACT_FILE:
|
||||
with open(APIEXTRACT_FILE, encoding="utf-8") as f:
|
||||
api = json.load(f)
|
||||
for l in api["results"]:
|
||||
if (not search and l["login"] == login) or (search and (("login" in l and l["login"].find(login) != -1) or ("cn" in l and l["cn"].find(login) != -1) or ("uid" in l and str(l["uid"]) == login))):
|
||||
yield Login(**l)
|
||||
|
||||
login_ = login + (":" if not search else "")
|
||||
lsize = len(login_)
|
||||
|
||||
with open(PASSWD_FILE, encoding="iso-8859-15") as f:
|
||||
for l in f.readlines():
|
||||
if l[:lsize] == login_:
|
||||
return Login(l.strip())
|
||||
return None
|
||||
if PASSWD_FILE:
|
||||
with open(PASSWD_FILE, encoding="iso-8859-15") as f:
|
||||
for l in f.readlines():
|
||||
if l[:lsize] == login_:
|
||||
yield Login(l.strip())
|
||||
|
||||
def cmd_whois(msg):
|
||||
if len(msg.args) < 1:
|
||||
raise IRCException("Provide a name")
|
||||
raise IMException("Provide a name")
|
||||
|
||||
res = Response(channel=msg.channel, count=" (%d more logins)")
|
||||
for srch in msg.args:
|
||||
l = found_login(srch)
|
||||
if l is not None:
|
||||
def format_response(t):
|
||||
srch, l = t
|
||||
if type(l) is Login:
|
||||
pic = l.get_photo()
|
||||
res.append_message("%s is %s (%s %s): %s%s" % (srch, l.cn.capitalize(), l.login, l.uid, l.get_promo(), " and looks like %s" % pic if pic is not None else ""))
|
||||
return "%s is %s (%s %s): %s%s" % (srch, l.cn.capitalize(), l.login, l.uid, l.get_promo(), " and looks like %s" % pic if pic is not None else "")
|
||||
else:
|
||||
res.append_message("Unknown %s :(" % srch)
|
||||
return l % srch
|
||||
|
||||
res = Response(channel=msg.channel, count=" (%d more logins)", line_treat=format_response)
|
||||
for srch in msg.args:
|
||||
found = False
|
||||
for l in login_lookup(srch, "lookup" in msg.kwargs):
|
||||
found = True
|
||||
res.append_message((srch, l))
|
||||
if not found:
|
||||
res.append_message((srch, "Unknown %s :("))
|
||||
return res
|
||||
|
||||
@hook("cmd_hook", "nicks")
|
||||
@hook.command("nicks")
|
||||
def cmd_nicks(msg):
|
||||
if len(msg.args) < 1:
|
||||
raise IRCException("Provide a login")
|
||||
nick = found_login(msg.args[0])
|
||||
raise IMException("Provide a login")
|
||||
nick = login_lookup(msg.args[0])
|
||||
if nick is None:
|
||||
nick = msg.args[0]
|
||||
else:
|
||||
|
|
@ -106,14 +145,14 @@ def cmd_nicks(msg):
|
|||
else:
|
||||
return Response("%s has no known alias." % nick, channel=msg.channel)
|
||||
|
||||
@hook("ask_default")
|
||||
@hook.ask()
|
||||
def parseask(msg):
|
||||
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.text, re.I)
|
||||
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.message, re.I)
|
||||
if res is not None:
|
||||
nick = res.group(1)
|
||||
login = res.group(3)
|
||||
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
|
||||
nick = msg.nick
|
||||
nick = msg.frm
|
||||
if nick in context.data.getNode("aliases").index:
|
||||
context.data.getNode("aliases").index[nick]["to"] = login
|
||||
else:
|
||||
|
|
@ -125,4 +164,4 @@ def parseask(msg):
|
|||
return Response("ok, c'est noté, %s est %s"
|
||||
% (nick, login),
|
||||
channel=msg.channel,
|
||||
nick=msg.nick)
|
||||
nick=msg.frm)
|
||||
|
|
|
|||
|
|
@ -1,99 +1,118 @@
|
|||
# coding=utf-8
|
||||
"""Performing search and calculation"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
|
||||
from urllib.parse import quote
|
||||
import re
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools import web
|
||||
|
||||
nemubotversion = 4.0
|
||||
from nemubot.module.more import Response
|
||||
|
||||
from more import Response
|
||||
|
||||
URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&appid=%s"
|
||||
# LOADING #############################################################
|
||||
|
||||
URL_API = "https://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s"
|
||||
|
||||
def load(context):
|
||||
global URL_API
|
||||
if not context.config or not context.config.hasAttribute("apikey"):
|
||||
if not context.config or "apikey" not in context.config:
|
||||
raise ImportError ("You need a Wolfram|Alpha API key in order to use "
|
||||
"this module. Add it to the module configuration: "
|
||||
"\n<module name=\"wolframalpha\" "
|
||||
"apikey=\"XXXXXX-XXXXXXXXXX\" />\n"
|
||||
"Register at http://products.wolframalpha.com/api/")
|
||||
"Register at https://products.wolframalpha.com/api/")
|
||||
URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%")
|
||||
|
||||
|
||||
class WFASearch:
|
||||
# MODULE CORE #########################################################
|
||||
|
||||
class WFAResults:
|
||||
|
||||
def __init__(self, terms):
|
||||
self.terms = terms
|
||||
self.wfares = web.getXML(URL_API % quote(terms))
|
||||
self.wfares = web.getXML(URL_API % quote(terms),
|
||||
timeout=12)
|
||||
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
try:
|
||||
return self.wfares["success"] == "true"
|
||||
return self.wfares.documentElement.hasAttribute("success") and self.wfares.documentElement.getAttribute("success") == "true"
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
if self.wfares is None:
|
||||
return "An error occurs during computation."
|
||||
elif self.wfares["error"] == "true":
|
||||
elif self.wfares.documentElement.hasAttribute("error") and self.wfares.documentElement.getAttribute("error") == "true":
|
||||
return ("An error occurs during computation: " +
|
||||
self.wfares.getNode("error").getNode("msg").getContent())
|
||||
elif self.wfares.hasNode("didyoumeans"):
|
||||
self.wfares.getElementsByTagName("error")[0].getElementsByTagName("msg")[0].firstChild.nodeValue)
|
||||
elif len(self.wfares.getElementsByTagName("didyoumeans")):
|
||||
start = "Did you mean: "
|
||||
tag = "didyoumean"
|
||||
end = "?"
|
||||
elif self.wfares.hasNode("tips"):
|
||||
elif len(self.wfares.getElementsByTagName("tips")):
|
||||
start = "Tips: "
|
||||
tag = "tip"
|
||||
end = ""
|
||||
elif self.wfares.hasNode("relatedexamples"):
|
||||
elif len(self.wfares.getElementsByTagName("relatedexamples")):
|
||||
start = "Related examples: "
|
||||
tag = "relatedexample"
|
||||
end = ""
|
||||
elif self.wfares.hasNode("futuretopic"):
|
||||
return self.wfares.getNode("futuretopic")["msg"]
|
||||
elif len(self.wfares.getElementsByTagName("futuretopic")):
|
||||
return self.wfares.getElementsByTagName("futuretopic")[0].getAttribute("msg")
|
||||
else:
|
||||
return "An error occurs during computation"
|
||||
|
||||
proposal = list()
|
||||
for dym in self.wfares.getNode(tag + "s").getNodes(tag):
|
||||
for dym in self.wfares.getElementsByTagName(tag):
|
||||
if tag == "tip":
|
||||
proposal.append(dym["text"])
|
||||
proposal.append(dym.getAttribute("text"))
|
||||
elif tag == "relatedexample":
|
||||
proposal.append(dym["desc"])
|
||||
proposal.append(dym.getAttribute("desc"))
|
||||
else:
|
||||
proposal.append(dym.getContent())
|
||||
proposal.append(dym.firstChild.nodeValue)
|
||||
|
||||
return start + ', '.join(proposal) + end
|
||||
|
||||
|
||||
@property
|
||||
def nextRes(self):
|
||||
try:
|
||||
for node in self.wfares.getNodes("pod"):
|
||||
for subnode in node.getNodes("subpod"):
|
||||
if subnode.getFirstNode("plaintext").getContent() != "":
|
||||
yield (node["title"] + " " + subnode["title"] + ": " +
|
||||
subnode.getFirstNode("plaintext").getContent())
|
||||
except IndexError:
|
||||
pass
|
||||
def results(self):
|
||||
for node in self.wfares.getElementsByTagName("pod"):
|
||||
for subnode in node.getElementsByTagName("subpod"):
|
||||
if subnode.getElementsByTagName("plaintext")[0].firstChild:
|
||||
yield (node.getAttribute("title") +
|
||||
((" / " + subnode.getAttribute("title")) if subnode.getAttribute("title") else "") + ": " +
|
||||
"; ".join(subnode.getElementsByTagName("plaintext")[0].firstChild.nodeValue.split("\n")))
|
||||
|
||||
|
||||
@hook("cmd_hook", "calculate")
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
@hook.command("calculate",
|
||||
help="Perform search and calculation using WolframAlpha",
|
||||
help_usage={
|
||||
"TERM": "Look at the given term on WolframAlpha",
|
||||
"CALCUL": "Perform the computation over WolframAlpha service",
|
||||
})
|
||||
def calculate(msg):
|
||||
if not len(msg.args):
|
||||
raise IRCException("Indicate a calcul to compute")
|
||||
raise IMException("Indicate a calcul to compute")
|
||||
|
||||
s = WFASearch(' '.join(msg.args))
|
||||
s = WFAResults(' '.join(msg.args))
|
||||
|
||||
if s.success:
|
||||
res = Response(channel=msg.channel, nomore="No more results")
|
||||
for result in s.nextRes:
|
||||
res.append_message(result)
|
||||
if (len(res.messages) > 0):
|
||||
res.messages.pop(0)
|
||||
return res
|
||||
else:
|
||||
return Response(s.error, msg.channel)
|
||||
if not s.success:
|
||||
raise IMException(s.error)
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more results")
|
||||
|
||||
for result in s.results:
|
||||
res.append_message(re.sub(r' +', ' ', result))
|
||||
if len(res.messages):
|
||||
res.messages.pop(0)
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""The 2014 football worldcup module"""
|
||||
"""The 2014,2018 football worldcup module"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from functools import partial
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
from urllib.request import urlopen
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.event import ModuleEvent
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
||||
nemubotversion = 3.4
|
||||
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
API_URL="http://worldcup.sfg.io/%s"
|
||||
|
||||
def load(context):
|
||||
from nemubot.event import ModuleEvent
|
||||
context.add_event(ModuleEvent(func=lambda url: urlopen(url, timeout=10).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30))
|
||||
context.add_event(ModuleEvent(func=partial(lambda url: urlopen(url, timeout=10).read().decode(), API_URL % "matches/current?by_date=DESC"), call=current_match_new_action, interval=30))
|
||||
|
||||
|
||||
def help_full ():
|
||||
|
|
@ -32,13 +33,13 @@ def start_watch(msg):
|
|||
w = ModuleState("watch")
|
||||
w["server"] = msg.server
|
||||
w["channel"] = msg.channel
|
||||
w["proprio"] = msg.nick
|
||||
w["proprio"] = msg.frm
|
||||
w["start"] = datetime.now(timezone.utc)
|
||||
context.data.addChild(w)
|
||||
context.save()
|
||||
raise IRCException("This channel is now watching world cup events!")
|
||||
raise IMException("This channel is now watching world cup events!")
|
||||
|
||||
@hook("cmd_hook", "watch_worldcup")
|
||||
@hook.command("watch_worldcup")
|
||||
def cmd_watch(msg):
|
||||
|
||||
# Get current state
|
||||
|
|
@ -52,23 +53,23 @@ def cmd_watch(msg):
|
|||
if msg.args[0] == "stop" and node is not None:
|
||||
context.data.delChild(node)
|
||||
context.save()
|
||||
raise IRCException("This channel will not anymore receives world cup events.")
|
||||
raise IMException("This channel will not anymore receives world cup events.")
|
||||
elif msg.args[0] == "start" and node is None:
|
||||
start_watch(msg)
|
||||
else:
|
||||
raise IRCException("Use only start or stop as first argument")
|
||||
raise IMException("Use only start or stop as first argument")
|
||||
else:
|
||||
if node is None:
|
||||
start_watch(msg)
|
||||
else:
|
||||
context.data.delChild(node)
|
||||
context.save()
|
||||
raise IRCException("This channel will not anymore receives world cup events.")
|
||||
raise IMException("This channel will not anymore receives world cup events.")
|
||||
|
||||
def current_match_new_action(match_str, osef):
|
||||
context.add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30))
|
||||
|
||||
matches = json.loads(match_str)
|
||||
def current_match_new_action(matches):
|
||||
def cmp(om, nm):
|
||||
return len(nm) and (len(om) == 0 or len(nm[0]["home_team_events"]) != len(om[0]["home_team_events"]) or len(nm[0]["away_team_events"]) != len(om[0]["away_team_events"]))
|
||||
context.add_event(ModuleEvent(func=partial(lambda url: json.loads(urlopen(url).read().decode()), API_URL % "matches/current?by_date=DESC"), cmp=partial(cmp, matches), call=current_match_new_action, interval=30))
|
||||
|
||||
for match in matches:
|
||||
if is_valid(match):
|
||||
|
|
@ -120,20 +121,19 @@ def detail_event(evt):
|
|||
return evt + " par"
|
||||
|
||||
def txt_event(e):
|
||||
return "%se minutes : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"])
|
||||
return "%s minute : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"])
|
||||
|
||||
def prettify(match):
|
||||
matchdate_local = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%S.%f%z")
|
||||
matchdate = matchdate_local - (matchdate_local.utcoffset() - datetime.timedelta(hours=2))
|
||||
matchdate = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%SZ").replace(tzinfo=timezone.utc)
|
||||
if match["status"] == "future":
|
||||
return ["Match à venir (%s) le %s : %s vs. %s" % (match["match_number"], matchdate.strftime("%A %d à %H:%M"), match["home_team"]["country"], match["away_team"]["country"])]
|
||||
return ["Match à venir (%s) le %s : %s vs. %s" % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M"), match["home_team"]["country"], match["away_team"]["country"])]
|
||||
else:
|
||||
msgs = list()
|
||||
msg = ""
|
||||
if match["status"] == "completed":
|
||||
msg += "Match (%s) du %s terminé : " % (match["match_number"], matchdate.strftime("%A %d à %H:%M"))
|
||||
msg += "Match (%s) du %s terminé : " % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M"))
|
||||
else:
|
||||
msg += "Match en cours (%s) depuis %d minutes : " % (match["match_number"], (datetime.now(matchdate.tzinfo) - matchdate_local).seconds / 60)
|
||||
msg += "Match en cours (%s) depuis %d minutes : " % (match["fifa_id"], (datetime.now(tz=timezone.utc) - matchdate).total_seconds() / 60)
|
||||
|
||||
msg += "%s %d - %d %s" % (match["home_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"], match["away_team"]["country"])
|
||||
|
||||
|
|
@ -163,21 +163,21 @@ def is_valid(match):
|
|||
def get_match(url, matchid):
|
||||
allm = get_matches(url)
|
||||
for m in allm:
|
||||
if int(m["match_number"]) == matchid:
|
||||
if int(m["fifa_id"]) == matchid:
|
||||
return [ m ]
|
||||
|
||||
def get_matches(url):
|
||||
try:
|
||||
raw = urlopen(url)
|
||||
except:
|
||||
raise IRCException("requête invalide")
|
||||
raise IMException("requête invalide")
|
||||
matches = json.loads(raw.read().decode())
|
||||
|
||||
for match in matches:
|
||||
if is_valid(match):
|
||||
yield match
|
||||
|
||||
@hook("cmd_hook", "worldcup")
|
||||
@hook.command("worldcup")
|
||||
def cmd_worldcup(msg):
|
||||
res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)")
|
||||
|
||||
|
|
@ -192,9 +192,9 @@ def cmd_worldcup(msg):
|
|||
elif len(msg.args[0]) == 3:
|
||||
url = "matches/country?fifa_code=%s&by_date=DESC" % msg.args[0]
|
||||
elif is_int(msg.args[0]):
|
||||
url = int(msg.arg[0])
|
||||
url = int(msg.args[0])
|
||||
else:
|
||||
raise IRCException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier")
|
||||
raise IMException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier")
|
||||
|
||||
if url is None:
|
||||
url = "matches/current?by_date=ASC"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from urllib.parse import urlparse
|
||||
import re, json, subprocess
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.web import _getNormalizedURL, getURLContent
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
|
||||
"""Get information of youtube videos"""
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ def _get_ytdl(links):
|
|||
res = []
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p:
|
||||
if p.wait() > 0:
|
||||
raise IRCException("Error while retrieving video information.")
|
||||
raise IMException("Error while retrieving video information.")
|
||||
for line in p.stdout.read().split(b"\n"):
|
||||
localres = ''
|
||||
if not line:
|
||||
|
|
@ -46,13 +46,13 @@ def _get_ytdl(links):
|
|||
localres += ' | ' + info['webpage_url']
|
||||
res.append(localres)
|
||||
if not res:
|
||||
raise IRCException("No video information to retrieve about this. Sorry!")
|
||||
raise IMException("No video information to retrieve about this. Sorry!")
|
||||
return res
|
||||
|
||||
LAST_URLS = dict()
|
||||
|
||||
|
||||
@hook("cmd_hook", "yt")
|
||||
@hook.command("yt")
|
||||
def get_info_yt(msg):
|
||||
links = list()
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ def get_info_yt(msg):
|
|||
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
|
||||
links.append(LAST_URLS[msg.channel].pop())
|
||||
else:
|
||||
raise IRCException("I don't have any youtube URL for now, please provide me one to get information!")
|
||||
raise IMException("I don't have any youtube URL for now, please provide me one to get information!")
|
||||
else:
|
||||
for url in msg.args:
|
||||
links.append(url)
|
||||
|
|
@ -73,23 +73,23 @@ def get_info_yt(msg):
|
|||
return res
|
||||
|
||||
|
||||
@hook("msg_default")
|
||||
@hook.message()
|
||||
def parselisten(msg):
|
||||
parseresponse(msg)
|
||||
return None
|
||||
|
||||
|
||||
@hook("all_post")
|
||||
@hook.post()
|
||||
def parseresponse(msg):
|
||||
global LAST_URLS
|
||||
if hasattr(msg, "text") and msg.text:
|
||||
if hasattr(msg, "text") and msg.text and type(msg.text) == str:
|
||||
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text)
|
||||
for url in urls:
|
||||
o = urlparse(_getNormalizedURL(url))
|
||||
if o.scheme != "":
|
||||
if o.netloc == "" and len(o.path) < 10:
|
||||
continue
|
||||
for recv in msg.receivers:
|
||||
for recv in msg.to:
|
||||
if recv not in LAST_URLS:
|
||||
LAST_URLS[recv] = list()
|
||||
LAST_URLS[recv].append(url)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
#
|
||||
|
|
@ -19,8 +17,9 @@
|
|||
__version__ = '4.0.dev3'
|
||||
__author__ = 'nemunaire'
|
||||
|
||||
from nemubot.modulecontext import ModuleContext
|
||||
context = ModuleContext(None, None)
|
||||
from nemubot.modulecontext import _ModuleContext
|
||||
|
||||
context = _ModuleContext()
|
||||
|
||||
|
||||
def requires_version(min=None, max=None):
|
||||
|
|
@ -40,50 +39,110 @@ def requires_version(min=None, max=None):
|
|||
"but this is nemubot v%s." % (str(max), __version__))
|
||||
|
||||
|
||||
def reload():
|
||||
"""Reload code of all Python modules used by nemubot
|
||||
def attach(pidfile, socketfile):
|
||||
import socket
|
||||
import sys
|
||||
|
||||
# Read PID from pidfile
|
||||
with open(pidfile, "r") as f:
|
||||
pid = int(f.readline())
|
||||
|
||||
print("nemubot is launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile))
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(socketfile)
|
||||
except socket.error as e:
|
||||
sys.stderr.write(str(e))
|
||||
sys.stderr.write("\n")
|
||||
return 1
|
||||
|
||||
import select
|
||||
mypoll = select.poll()
|
||||
|
||||
mypoll.register(sys.stdin.fileno(), select.POLLIN | select.POLLPRI)
|
||||
mypoll.register(sock.fileno(), select.POLLIN | select.POLLPRI)
|
||||
try:
|
||||
while True:
|
||||
for fd, flag in mypoll.poll():
|
||||
if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
|
||||
sock.close()
|
||||
print("Connection closed.")
|
||||
return 1
|
||||
|
||||
if fd == sys.stdin.fileno():
|
||||
line = sys.stdin.readline().strip()
|
||||
if line == "exit" or line == "quit":
|
||||
return 0
|
||||
elif line == "reload":
|
||||
import os, signal
|
||||
os.kill(pid, signal.SIGHUP)
|
||||
print("Reload signal sent. Please wait...")
|
||||
|
||||
elif line == "shutdown":
|
||||
import os, signal
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
print("Shutdown signal sent. Please wait...")
|
||||
|
||||
elif line == "kill":
|
||||
import os, signal
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
print("Signal sent...")
|
||||
return 0
|
||||
|
||||
elif line == "stack" or line == "stacks":
|
||||
import os, signal
|
||||
os.kill(pid, signal.SIGUSR1)
|
||||
print("Debug signal sent. Consult logs.")
|
||||
|
||||
else:
|
||||
sock.send(line.encode() + b'\r\n')
|
||||
|
||||
if fd == sock.fileno():
|
||||
sys.stdout.write(sock.recv(2048).decode())
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except:
|
||||
return 1
|
||||
finally:
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
def daemonize(socketfile=None):
|
||||
"""Detach the running process to run as a daemon
|
||||
"""
|
||||
|
||||
import imp
|
||||
import os
|
||||
import sys
|
||||
|
||||
import nemubot.channel
|
||||
imp.reload(nemubot.channel)
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as err:
|
||||
sys.stderr.write("Unable to fork: %s\n" % err)
|
||||
sys.exit(1)
|
||||
|
||||
import nemubot.consumer
|
||||
imp.reload(nemubot.consumer)
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
os.chdir('/')
|
||||
|
||||
import nemubot.event
|
||||
imp.reload(nemubot.event)
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as err:
|
||||
sys.stderr.write("Unable to fork: %s\n" % err)
|
||||
sys.exit(1)
|
||||
|
||||
import nemubot.exception
|
||||
imp.reload(nemubot.exception)
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
si = open(os.devnull, 'r')
|
||||
so = open(os.devnull, 'a+')
|
||||
se = open(os.devnull, 'a+')
|
||||
|
||||
import nemubot.hooks
|
||||
imp.reload(nemubot.hooks)
|
||||
|
||||
nemubot.hooks.reload()
|
||||
|
||||
import nemubot.importer
|
||||
imp.reload(nemubot.importer)
|
||||
|
||||
import nemubot.message
|
||||
imp.reload(nemubot.message)
|
||||
|
||||
nemubot.message.reload()
|
||||
|
||||
import nemubot.prompt
|
||||
imp.reload(nemubot.prompt)
|
||||
|
||||
nemubot.prompt.reload()
|
||||
|
||||
import nemubot.server
|
||||
rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist
|
||||
imp.reload(nemubot.server)
|
||||
nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist = rl, wl, xl
|
||||
|
||||
nemubot.server.reload()
|
||||
|
||||
import nemubot.tools
|
||||
imp.reload(nemubot.tools)
|
||||
|
||||
nemubot.tools.reload()
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# Copyright (C) 2012-2017 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
|
||||
|
|
@ -18,6 +16,7 @@
|
|||
|
||||
def main():
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
# Parse command line arguments
|
||||
|
|
@ -38,6 +37,18 @@ def main():
|
|||
default=["./modules/"],
|
||||
help="directory to use as modules store")
|
||||
|
||||
parser.add_argument("-A", "--no-attach", action="store_true",
|
||||
help="don't attach after fork")
|
||||
|
||||
parser.add_argument("-d", "--debug", action="store_true",
|
||||
help="don't deamonize, keep in foreground")
|
||||
|
||||
parser.add_argument("-P", "--pidfile", default="./nemubot.pid",
|
||||
help="Path to the file where store PID")
|
||||
|
||||
parser.add_argument("-S", "--socketfile", default="./nemubot.sock",
|
||||
help="path where open the socket for internal communication")
|
||||
|
||||
parser.add_argument("-l", "--logfile", default="./nemubot.log",
|
||||
help="Path to store logs")
|
||||
|
||||
|
|
@ -60,11 +71,27 @@ def main():
|
|||
|
||||
# Resolve relatives paths
|
||||
args.data_path = os.path.abspath(os.path.expanduser(args.data_path))
|
||||
args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) if args.pidfile is not None and args.pidfile != "" else None
|
||||
args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile)) if args.socketfile is not None and args.socketfile != "" else None
|
||||
args.logfile = os.path.abspath(os.path.expanduser(args.logfile))
|
||||
args.files = [ x for x in map(os.path.abspath, args.files)]
|
||||
args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)]
|
||||
args.files = [x for x in map(os.path.abspath, args.files)]
|
||||
args.modules_path = [x for x in map(os.path.abspath, args.modules_path)]
|
||||
|
||||
# Setup loggin interface
|
||||
# Prepare the attached client, before setting other stuff
|
||||
if not args.debug and not args.no_attach and args.socketfile is not None and args.pidfile is not None:
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
import time
|
||||
os.waitpid(pid, 0)
|
||||
time.sleep(1)
|
||||
from nemubot import attach
|
||||
sys.exit(attach(args.pidfile, args.socketfile))
|
||||
except OSError as err:
|
||||
sys.stderr.write("Unable to fork: %s\n" % err)
|
||||
sys.exit(1)
|
||||
|
||||
# Setup logging interface
|
||||
import logging
|
||||
logger = logging.getLogger("nemubot")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
|
@ -72,16 +99,29 @@ def main():
|
|||
formatter = logging.Formatter(
|
||||
'%(asctime)s %(name)s %(levelname)s %(message)s')
|
||||
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
if args.verbose < 2:
|
||||
ch.setLevel(logging.INFO)
|
||||
logger.addHandler(ch)
|
||||
if args.debug:
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
if args.verbose < 2:
|
||||
ch.setLevel(logging.INFO)
|
||||
logger.addHandler(ch)
|
||||
|
||||
fh = logging.FileHandler(args.logfile)
|
||||
fh.setFormatter(formatter)
|
||||
logger.addHandler(fh)
|
||||
|
||||
# Check if an instance is already launched
|
||||
if args.pidfile is not None and os.path.isfile(args.pidfile):
|
||||
with open(args.pidfile, "r") as f:
|
||||
pid = int(f.readline())
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
from nemubot import attach
|
||||
sys.exit(attach(args.pidfile, args.socketfile))
|
||||
|
||||
# Add modules dir paths
|
||||
modules_paths = list()
|
||||
for path in args.modules_path:
|
||||
|
|
@ -95,61 +135,145 @@ def main():
|
|||
from nemubot.bot import Bot
|
||||
context = Bot(modules_paths=modules_paths,
|
||||
data_store=datastore.XML(args.data_path),
|
||||
verbosity=args.verbose)
|
||||
debug=args.verbose > 0)
|
||||
|
||||
if args.no_connect:
|
||||
context.noautoconnect = True
|
||||
|
||||
# Load the prompt
|
||||
import nemubot.prompt
|
||||
prmpt = nemubot.prompt.Prompt()
|
||||
|
||||
# Register the hook for futur import
|
||||
from nemubot.importer import ModuleFinder
|
||||
sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module))
|
||||
module_finder = ModuleFinder(context.modules_paths, context.add_module)
|
||||
sys.meta_path.append(module_finder)
|
||||
|
||||
# Load requested configuration files
|
||||
for path in args.files:
|
||||
if os.path.isfile(path):
|
||||
context.sync_queue.put_nowait(["loadconf", path])
|
||||
else:
|
||||
if not os.path.isfile(path):
|
||||
logger.error("%s is not a readable file", path)
|
||||
continue
|
||||
|
||||
config = load_config(path)
|
||||
|
||||
# Preset each server in this file
|
||||
for server in config.servers:
|
||||
# Add the server in the context
|
||||
for i in [0,1,2,3]:
|
||||
srv = server.server(config, trynb=i)
|
||||
try:
|
||||
if context.add_server(srv):
|
||||
logger.info("Server '%s' successfully added.", srv.name)
|
||||
else:
|
||||
logger.error("Can't add server '%s'.", srv.name)
|
||||
except Exception as e:
|
||||
logger.error("Unable to connect to '%s': %s", srv.name, e)
|
||||
continue
|
||||
break
|
||||
|
||||
# Load module and their configuration
|
||||
for mod in config.modules:
|
||||
context.modules_configuration[mod.name] = mod
|
||||
if mod.autoload:
|
||||
try:
|
||||
__import__("nemubot.module." + mod.name)
|
||||
except:
|
||||
logger.exception("Exception occurs when loading module"
|
||||
" '%s'", mod.name)
|
||||
|
||||
# Load files asked by the configuration file
|
||||
args.files += config.includes
|
||||
|
||||
|
||||
if args.module:
|
||||
for module in args.module:
|
||||
__import__(module)
|
||||
__import__("nemubot.module." + module)
|
||||
|
||||
print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__,
|
||||
os.getpid()))
|
||||
while True:
|
||||
from nemubot.prompt.reset import PromptReset
|
||||
try:
|
||||
context.start()
|
||||
if prmpt.run(context):
|
||||
break
|
||||
except PromptReset as e:
|
||||
if e.type == "quit":
|
||||
break
|
||||
if args.socketfile:
|
||||
from nemubot.server.socket import UnixSocketListener
|
||||
context.add_server(UnixSocketListener(new_server_cb=context.add_server,
|
||||
location=args.socketfile,
|
||||
name="master_socket"))
|
||||
|
||||
try:
|
||||
import imp
|
||||
# Reload all other modules
|
||||
imp.reload(nemubot)
|
||||
imp.reload(nemubot.prompt)
|
||||
nemubot.reload()
|
||||
import nemubot.bot
|
||||
context = nemubot.bot.hotswap(context)
|
||||
prmpt = nemubot.prompt.hotswap(prmpt)
|
||||
print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" %
|
||||
nemubot.__version__)
|
||||
except:
|
||||
logger.exception("\033[1;31mUnable to reload the prompt due to "
|
||||
"errors.\033[0m Fix them before trying to reload "
|
||||
"the prompt.")
|
||||
# Daemonize
|
||||
if not args.debug:
|
||||
from nemubot import daemonize
|
||||
daemonize(args.socketfile)
|
||||
|
||||
context.quit()
|
||||
print("Waiting for other threads shuts down...")
|
||||
# Signals handling
|
||||
def sigtermhandler(signum, frame):
|
||||
"""On SIGTERM and SIGINT, quit nicely"""
|
||||
context.quit()
|
||||
signal.signal(signal.SIGINT, sigtermhandler)
|
||||
signal.signal(signal.SIGTERM, sigtermhandler)
|
||||
|
||||
def sighuphandler(signum, frame):
|
||||
"""On SIGHUP, perform a deep reload"""
|
||||
nonlocal context
|
||||
|
||||
logger.debug("SIGHUP receive, iniate reload procedure...")
|
||||
|
||||
# Reload configuration file
|
||||
for path in args.files:
|
||||
if os.path.isfile(path):
|
||||
sync_act("loadconf", path)
|
||||
signal.signal(signal.SIGHUP, sighuphandler)
|
||||
|
||||
def sigusr1handler(signum, frame):
|
||||
"""On SIGHUSR1, display stacktraces"""
|
||||
import threading, traceback
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
thName = "#%d" % threadId
|
||||
for th in threading.enumerate():
|
||||
if th.ident == threadId:
|
||||
thName = th.name
|
||||
break
|
||||
logger.debug("########### Thread %s:\n%s",
|
||||
thName,
|
||||
"".join(traceback.format_stack(stack)))
|
||||
signal.signal(signal.SIGUSR1, sigusr1handler)
|
||||
|
||||
# Store PID to pidfile
|
||||
if args.pidfile is not None:
|
||||
with open(args.pidfile, "w+") as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
# context can change when performing an hotswap, always join the latest context
|
||||
oldcontext = None
|
||||
while oldcontext != context:
|
||||
oldcontext = context
|
||||
context.start()
|
||||
context.join()
|
||||
|
||||
# Wait for consumers
|
||||
logger.info("Waiting for other threads shuts down...")
|
||||
if args.debug:
|
||||
sigusr1handler(0, None)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def load_config(filename):
|
||||
"""Load a configuration file
|
||||
|
||||
Arguments:
|
||||
filename -- the path to the file to load
|
||||
"""
|
||||
|
||||
from nemubot.channel import Channel
|
||||
from nemubot import config
|
||||
from nemubot.tools.xmlparser import XMLParser
|
||||
|
||||
try:
|
||||
p = XMLParser({
|
||||
"nemubotconfig": config.Nemubot,
|
||||
"server": config.Server,
|
||||
"channel": Channel,
|
||||
"module": config.Module,
|
||||
"include": config.Include,
|
||||
})
|
||||
return p.parse_file(filename)
|
||||
except:
|
||||
logger.exception("Can't load `%s'; this is not a valid nemubot "
|
||||
"configuration file.", filename)
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
350
nemubot/bot.py
350
nemubot/bot.py
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
@ -16,10 +14,13 @@
|
|||
# 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, timedelta, timezone
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
from multiprocessing import JoinableQueue
|
||||
import threading
|
||||
import select
|
||||
import sys
|
||||
import weakref
|
||||
|
||||
from nemubot import __version__
|
||||
from nemubot.consumer import Consumer, EventConsumer, MessageConsumer
|
||||
|
|
@ -28,27 +29,35 @@ import nemubot.hooks
|
|||
|
||||
logger = logging.getLogger("nemubot")
|
||||
|
||||
sync_queue = JoinableQueue()
|
||||
|
||||
def sync_act(*args):
|
||||
sync_queue.put(list(args))
|
||||
|
||||
|
||||
class Bot(threading.Thread):
|
||||
|
||||
"""Class containing the bot context and ensuring key goals"""
|
||||
|
||||
def __init__(self, ip="127.0.0.1", modules_paths=list(),
|
||||
data_store=datastore.Abstract(), verbosity=0):
|
||||
data_store=datastore.Abstract(), debug=False):
|
||||
"""Initialize the bot context
|
||||
|
||||
Keyword arguments:
|
||||
ip -- The external IP of the bot (default: 127.0.0.1)
|
||||
modules_paths -- Paths to all directories where looking for module
|
||||
modules_paths -- Paths to all directories where looking for modules
|
||||
data_store -- An instance of the nemubot datastore for bot's modules
|
||||
debug -- enable debug
|
||||
"""
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
super().__init__(name="Nemubot main")
|
||||
|
||||
logger.info("Initiate nemubot v%s", __version__)
|
||||
logger.info("Initiate nemubot v%s (running on Python %s.%s.%s)",
|
||||
__version__,
|
||||
sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
|
||||
|
||||
self.verbosity = verbosity
|
||||
self.stop = None
|
||||
self.debug = debug
|
||||
self.stop = True
|
||||
|
||||
# External IP for accessing this bot
|
||||
import ipaddress
|
||||
|
|
@ -60,6 +69,7 @@ class Bot(threading.Thread):
|
|||
self.datastore.open()
|
||||
|
||||
# Keep global context: servers and modules
|
||||
self._poll = select.poll()
|
||||
self.servers = dict()
|
||||
self.modules = dict()
|
||||
self.modules_configuration = dict()
|
||||
|
|
@ -74,41 +84,44 @@ class Bot(threading.Thread):
|
|||
|
||||
import re
|
||||
def in_ping(msg):
|
||||
if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.message, re.I) is not None:
|
||||
return msg.respond("pong")
|
||||
self.treater.hm.add_hook(nemubot.hooks.Message(in_ping), "in", "DirectAsk")
|
||||
return msg.respond("pong")
|
||||
self.treater.hm.add_hook(nemubot.hooks.Message(in_ping,
|
||||
match=lambda msg: re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
|
||||
msg.message, re.I)),
|
||||
"in", "DirectAsk")
|
||||
|
||||
def in_echo(msg):
|
||||
from nemubot.message import Text
|
||||
return Text(msg.nick + ": " + " ".join(msg.args), to=msg.to_response)
|
||||
self.treater.hm.add_hook(nemubot.hooks.Message(in_echo, "echo"), "in", "Command")
|
||||
return Text(msg.frm + ": " + " ".join(msg.args), to=msg.to_response)
|
||||
self.treater.hm.add_hook(nemubot.hooks.Command(in_echo, "echo"), "in", "Command")
|
||||
|
||||
def _help_msg(msg):
|
||||
"""Parse and response to help messages"""
|
||||
from more import Response
|
||||
from nemubot.module.more import Response
|
||||
res = Response(channel=msg.to_response)
|
||||
if len(msg.args) >= 1:
|
||||
if msg.args[0] in self.modules:
|
||||
if hasattr(self.modules[msg.args[0]], "help_full"):
|
||||
hlp = self.modules[msg.args[0]].help_full()
|
||||
if "nemubot.module." + msg.args[0] in self.modules and self.modules["nemubot.module." + msg.args[0]]() is not None:
|
||||
mname = "nemubot.module." + msg.args[0]
|
||||
if hasattr(self.modules[mname](), "help_full"):
|
||||
hlp = self.modules[mname]().help_full()
|
||||
if isinstance(hlp, Response):
|
||||
return hlp
|
||||
else:
|
||||
res.append_message(hlp)
|
||||
else:
|
||||
res.append_message([str(h) for s,h in self.modules[msg.args[0]].__nemubot_context__.hooks], title="Available commands for module " + msg.args[0])
|
||||
res.append_message([str(h) for s,h in self.modules[mname]().__nemubot_context__.hooks], title="Available commands for module " + msg.args[0])
|
||||
elif msg.args[0][0] == "!":
|
||||
for module in self.modules:
|
||||
for (s, h) in self.modules[module].__nemubot_context__.hooks:
|
||||
if s == "in_Command" and (h.name is not None or h.regexp is not None) and h.is_matching(msg.args[0][1:]):
|
||||
if h.help_usage:
|
||||
return res.append_message(["\x03\x02%s%s\x03\x02: %s" % (msg.args[0], " " + (k if k is not None else ""), h.help_usage[k]) for k in h.help_usage], title="Usage for command %s from module %s" % (msg.args[0], module))
|
||||
elif h.help:
|
||||
return res.append_message("Command %s from module %s: %s" % (msg.args[0], module, h.help))
|
||||
else:
|
||||
return res.append_message("Sorry, there is currently no help for the command %s. Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare" % msg.args[0])
|
||||
else:
|
||||
res.append_message("Sorry, there is no command %s" % msg.args[0])
|
||||
from nemubot.message.command import Command
|
||||
for h in self.treater._in_hooks(Command(msg.args[0][1:])):
|
||||
if h.help_usage:
|
||||
lp = ["\x03\x02%s%s\x03\x02: %s" % (msg.args[0], (" " + k if k is not None else ""), h.help_usage[k]) for k in h.help_usage]
|
||||
jp = h.keywords.help()
|
||||
return res.append_message(lp + ([". Moreover, you can provides some optional parameters: "] + jp if len(jp) else []), title="Usage for command %s" % msg.args[0])
|
||||
elif h.help:
|
||||
return res.append_message("Command %s: %s" % (msg.args[0], h.help))
|
||||
else:
|
||||
return res.append_message("Sorry, there is currently no help for the command %s. Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare" % msg.args[0])
|
||||
res.append_message("Sorry, there is no command %s" % msg.args[0])
|
||||
else:
|
||||
res.append_message("Sorry, there is no module named %s" % msg.args[0])
|
||||
else:
|
||||
|
|
@ -122,92 +135,121 @@ class Bot(threading.Thread):
|
|||
"Vous pouvez le consulter, le dupliquer, "
|
||||
"envoyer des rapports de bogues ou bien "
|
||||
"contribuer au projet sur GitHub : "
|
||||
"http://github.com/nemunaire/nemubot/")
|
||||
"https://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, self.modules[im].__doc__) for im in self.modules if self.modules[im].__doc__])
|
||||
message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im]().__doc__) for im in self.modules if self.modules[im]() is not None and self.modules[im]().__doc__])
|
||||
return res
|
||||
self.treater.hm.add_hook(nemubot.hooks.Message(_help_msg, "help"), "in", "Command")
|
||||
self.treater.hm.add_hook(nemubot.hooks.Command(_help_msg, "help"), "in", "Command")
|
||||
|
||||
import os
|
||||
from queue import Queue
|
||||
# Messages to be treated
|
||||
self.cnsr_queue = Queue()
|
||||
self.cnsr_thrd = list()
|
||||
self.cnsr_thrd_size = -1
|
||||
# Synchrone actions to be treated by main thread
|
||||
self.sync_queue = Queue()
|
||||
# Messages to be treated — shared across all server connections.
|
||||
# cnsr_active tracks consumers currently inside stm.run() (not idle),
|
||||
# which lets us spawn a new thread the moment all existing ones are busy.
|
||||
self.cnsr_queue = Queue()
|
||||
self.cnsr_thrd = list()
|
||||
self.cnsr_lock = threading.Lock()
|
||||
self.cnsr_active = 0 # consumers currently executing a task
|
||||
self.cnsr_max = os.cpu_count() or 4 # upper bound on concurrent consumer threads
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self.datastore.close()
|
||||
|
||||
|
||||
def run(self):
|
||||
from select import select
|
||||
from nemubot.server import _lock, _rlist, _wlist, _xlist
|
||||
global sync_queue
|
||||
|
||||
# Rewrite the sync_queue, as the daemonization process tend to disturb it
|
||||
old_sync_queue, sync_queue = sync_queue, JoinableQueue()
|
||||
while not old_sync_queue.empty():
|
||||
sync_queue.put_nowait(old_sync_queue.get())
|
||||
|
||||
self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI)
|
||||
|
||||
|
||||
self.stop = False
|
||||
|
||||
# Relaunch events
|
||||
self._update_event_timer()
|
||||
|
||||
logger.info("Starting main loop")
|
||||
while not self.stop:
|
||||
with _lock:
|
||||
try:
|
||||
rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1)
|
||||
except:
|
||||
logger.error("Something went wrong in select")
|
||||
fnd_smth = False
|
||||
# Looking for invalid server
|
||||
for r in _rlist:
|
||||
if not hasattr(r, "fileno") or not isinstance(r.fileno(), int) or r.fileno() < 0:
|
||||
_rlist.remove(r)
|
||||
logger.error("Found invalid object in _rlist: " + str(r))
|
||||
fnd_smth = True
|
||||
for w in _wlist:
|
||||
if not hasattr(w, "fileno") or not isinstance(w.fileno(), int) or w.fileno() < 0:
|
||||
_wlist.remove(w)
|
||||
logger.error("Found invalid object in _wlist: " + str(w))
|
||||
fnd_smth = True
|
||||
for x in _xlist:
|
||||
if not hasattr(x, "fileno") or not isinstance(x.fileno(), int) or x.fileno() < 0:
|
||||
_xlist.remove(x)
|
||||
logger.error("Found invalid object in _xlist: " + str(x))
|
||||
fnd_smth = True
|
||||
if not fnd_smth:
|
||||
logger.exception("Can't continue, sorry")
|
||||
self.quit()
|
||||
continue
|
||||
for fd, flag in self._poll.poll():
|
||||
# Handle internal socket passing orders
|
||||
if fd != sync_queue._reader.fileno() and fd in self.servers:
|
||||
srv = self.servers[fd]
|
||||
|
||||
for x in xl:
|
||||
try:
|
||||
x.exception()
|
||||
except:
|
||||
logger.exception("Uncatched exception on server exception")
|
||||
for w in wl:
|
||||
try:
|
||||
w.write_select()
|
||||
except:
|
||||
logger.exception("Uncatched exception on server write")
|
||||
for r in rl:
|
||||
for i in r.read():
|
||||
if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
|
||||
try:
|
||||
self.receive_message(r, i)
|
||||
srv.exception(flag)
|
||||
except:
|
||||
logger.exception("Uncatched exception on server read")
|
||||
logger.exception("Uncatched exception on server exception")
|
||||
|
||||
if srv.fileno() > 0:
|
||||
if flag & (select.POLLOUT):
|
||||
try:
|
||||
srv.async_write()
|
||||
except:
|
||||
logger.exception("Uncatched exception on server write")
|
||||
|
||||
if flag & (select.POLLIN | select.POLLPRI):
|
||||
try:
|
||||
for i in srv.async_read():
|
||||
self.receive_message(srv, i)
|
||||
except:
|
||||
logger.exception("Uncatched exception on server read")
|
||||
|
||||
else:
|
||||
del self.servers[fd]
|
||||
|
||||
|
||||
# Launch new consumer threads if necessary
|
||||
while self.cnsr_queue.qsize() > self.cnsr_thrd_size:
|
||||
# Next launch if two more items in queue
|
||||
self.cnsr_thrd_size += 2
|
||||
# Always check the sync queue
|
||||
while not sync_queue.empty():
|
||||
args = sync_queue.get()
|
||||
action = args.pop(0)
|
||||
|
||||
c = Consumer(self)
|
||||
self.cnsr_thrd.append(c)
|
||||
c.start()
|
||||
logger.debug("Executing sync_queue action %s%s", action, args)
|
||||
|
||||
while self.sync_queue.qsize() > 0:
|
||||
action = self.sync_queue.get_nowait()
|
||||
if action[0] == "exit":
|
||||
self.quit()
|
||||
elif action[0] == "loadconf":
|
||||
for path in action[1:]:
|
||||
from nemubot.tools.config import load_file
|
||||
load_file(path, self)
|
||||
self.sync_queue.task_done()
|
||||
if action == "sckt" and len(args) >= 2:
|
||||
try:
|
||||
if args[0] == "write":
|
||||
self._poll.modify(int(args[1]), select.POLLOUT | select.POLLIN | select.POLLPRI)
|
||||
elif args[0] == "unwrite":
|
||||
self._poll.modify(int(args[1]), select.POLLIN | select.POLLPRI)
|
||||
|
||||
elif args[0] == "register":
|
||||
self._poll.register(int(args[1]), select.POLLIN | select.POLLPRI)
|
||||
elif args[0] == "unregister":
|
||||
try:
|
||||
self._poll.unregister(int(args[1]))
|
||||
except KeyError:
|
||||
pass
|
||||
except:
|
||||
logger.exception("Unhandled excpetion during action:")
|
||||
|
||||
elif action == "exit":
|
||||
self.quit()
|
||||
|
||||
elif action == "launch_consumer":
|
||||
pass # This is treated after the loop
|
||||
|
||||
sync_queue.task_done()
|
||||
|
||||
|
||||
# Spawn a new consumer whenever the queue has work and every
|
||||
# existing consumer is already busy executing a task.
|
||||
with self.cnsr_lock:
|
||||
while (not self.cnsr_queue.empty()
|
||||
and self.cnsr_active >= len(self.cnsr_thrd)
|
||||
and len(self.cnsr_thrd) < self.cnsr_max):
|
||||
c = Consumer(self)
|
||||
self.cnsr_thrd.append(c)
|
||||
c.start()
|
||||
sync_queue = None
|
||||
logger.info("Ending main loop")
|
||||
|
||||
|
||||
|
||||
|
|
@ -228,10 +270,6 @@ class Bot(threading.Thread):
|
|||
module_src -- The module to which the event is attached to
|
||||
"""
|
||||
|
||||
if hasattr(self, "stop") and self.stop:
|
||||
logger.warn("The bot is stopped, can't register new events")
|
||||
return
|
||||
|
||||
import uuid
|
||||
|
||||
# Generate the event id if no given
|
||||
|
|
@ -242,7 +280,7 @@ class Bot(threading.Thread):
|
|||
if type(eid) is uuid.UUID:
|
||||
evt.id = str(eid)
|
||||
else:
|
||||
# Ok, this is quite useless...
|
||||
# Ok, this is quiet useless...
|
||||
try:
|
||||
evt.id = str(uuid.UUID(eid))
|
||||
except ValueError:
|
||||
|
|
@ -258,7 +296,7 @@ class Bot(threading.Thread):
|
|||
break
|
||||
self.events.insert(i, evt)
|
||||
|
||||
if i == 0:
|
||||
if i == 0 and not self.stop:
|
||||
# First event changed, reset timer
|
||||
self._update_event_timer()
|
||||
if len(self.events) <= 0 or self.events[i] != evt:
|
||||
|
|
@ -267,10 +305,10 @@ class Bot(threading.Thread):
|
|||
|
||||
# Register the event in the source module
|
||||
if module_src is not None:
|
||||
module_src.__nemubot_context__.events.append(evt.id)
|
||||
module_src.__nemubot_context__.events.append((evt, evt.id))
|
||||
evt.module_src = module_src
|
||||
|
||||
logger.info("New event registered: %s -> %s", evt.id, evt)
|
||||
logger.info("New event registered in %d position: %s", i, t)
|
||||
return evt.id
|
||||
|
||||
|
||||
|
|
@ -297,10 +335,10 @@ class Bot(threading.Thread):
|
|||
id = evt
|
||||
|
||||
if len(self.events) > 0 and id == self.events[0].id:
|
||||
if module_src is not None:
|
||||
module_src.__nemubot_context__.events.remove((self.events[0], id))
|
||||
self.events.remove(self.events[0])
|
||||
self._update_event_timer()
|
||||
if module_src is not None:
|
||||
module_src.__nemubot_context__.events.remove(id)
|
||||
return True
|
||||
|
||||
for evt in self.events:
|
||||
|
|
@ -308,7 +346,7 @@ class Bot(threading.Thread):
|
|||
self.events.remove(evt)
|
||||
|
||||
if module_src is not None:
|
||||
module_src.__nemubot_context__.events.remove(evt.id)
|
||||
module_src.__nemubot_context__.events.remove((evt, evt.id))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -321,11 +359,15 @@ class Bot(threading.Thread):
|
|||
self.event_timer.cancel()
|
||||
|
||||
if len(self.events):
|
||||
logger.debug("Update timer: next event in %d seconds",
|
||||
self.events[0].time_left.seconds)
|
||||
self.event_timer = threading.Timer(
|
||||
self.events[0].time_left.seconds + self.events[0].time_left.microseconds / 1000000 if datetime.now(timezone.utc) < self.events[0].current else 0,
|
||||
self._end_event_timer)
|
||||
try:
|
||||
remaining = self.events[0].time_left.total_seconds()
|
||||
except:
|
||||
logger.exception("An error occurs during event time calculation:")
|
||||
self.events.pop(0)
|
||||
return self._update_event_timer()
|
||||
|
||||
logger.debug("Update timer: next event in %d seconds", remaining)
|
||||
self.event_timer = threading.Timer(remaining if remaining > 0 else 0, self._end_event_timer)
|
||||
self.event_timer.start()
|
||||
|
||||
else:
|
||||
|
|
@ -338,6 +380,7 @@ class Bot(threading.Thread):
|
|||
while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current:
|
||||
evt = self.events.pop(0)
|
||||
self.cnsr_queue.put_nowait(EventConsumer(evt))
|
||||
sync_act("launch_consumer")
|
||||
|
||||
self._update_event_timer()
|
||||
|
||||
|
|
@ -352,10 +395,12 @@ class Bot(threading.Thread):
|
|||
autoconnect -- connect after add?
|
||||
"""
|
||||
|
||||
if srv.id not in self.servers:
|
||||
self.servers[srv.id] = srv
|
||||
fileno = srv.fileno()
|
||||
if fileno not in self.servers:
|
||||
self.servers[fileno] = srv
|
||||
self.servers[srv.name] = srv
|
||||
if autoconnect and not hasattr(self, "noautoconnect"):
|
||||
srv.open()
|
||||
srv.connect()
|
||||
return True
|
||||
|
||||
else:
|
||||
|
|
@ -382,10 +427,6 @@ class Bot(threading.Thread):
|
|||
old one before"""
|
||||
module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__
|
||||
|
||||
if hasattr(self, "stop") and self.stop:
|
||||
logger.warn("The bot is stopped, can't register new modules")
|
||||
return
|
||||
|
||||
# Check if the module already exists
|
||||
if module_name in self.modules:
|
||||
self.unload_module(module_name)
|
||||
|
|
@ -399,7 +440,7 @@ class Bot(threading.Thread):
|
|||
module.print = prnt
|
||||
|
||||
# Create module context
|
||||
from nemubot.modulecontext import ModuleContext
|
||||
from nemubot.modulecontext import _ModuleContext, ModuleContext
|
||||
module.__nemubot_context__ = ModuleContext(self, module)
|
||||
|
||||
if not hasattr(module, "logger"):
|
||||
|
|
@ -407,14 +448,14 @@ class Bot(threading.Thread):
|
|||
|
||||
# Replace imported context by real one
|
||||
for attr in module.__dict__:
|
||||
if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext:
|
||||
if attr != "__nemubot_context__" and type(module.__dict__[attr]) == _ModuleContext:
|
||||
module.__dict__[attr] = module.__nemubot_context__
|
||||
|
||||
# Register decorated functions
|
||||
import nemubot.hooks
|
||||
for s, h in nemubot.hooks.last_registered:
|
||||
module.__nemubot_context__.add_hook(s, h)
|
||||
nemubot.hooks.last_registered = []
|
||||
for s, h in nemubot.hooks.hook.last_registered:
|
||||
module.__nemubot_context__.add_hook(h, *s if isinstance(s, list) else s)
|
||||
nemubot.hooks.hook.last_registered = []
|
||||
|
||||
# Launch the module
|
||||
if hasattr(module, "load"):
|
||||
|
|
@ -425,18 +466,20 @@ class Bot(threading.Thread):
|
|||
raise
|
||||
|
||||
# Save a reference to the module
|
||||
self.modules[module_name] = module
|
||||
self.modules[module_name] = weakref.ref(module)
|
||||
logger.info("Module '%s' successfully loaded.", module_name)
|
||||
|
||||
|
||||
def unload_module(self, name):
|
||||
"""Unload a module"""
|
||||
if name in self.modules:
|
||||
self.modules[name].print("Unloading module %s" % name)
|
||||
if name in self.modules and self.modules[name]() is not None:
|
||||
module = self.modules[name]()
|
||||
module.print("Unloading module %s" % name)
|
||||
|
||||
# Call the user defined unload method
|
||||
if hasattr(self.modules[name], "unload"):
|
||||
self.modules[name].unload(self)
|
||||
self.modules[name].__nemubot_context__.unload()
|
||||
if hasattr(module, "unload"):
|
||||
module.unload(self)
|
||||
module.__nemubot_context__.unload()
|
||||
|
||||
# Remove from the nemubot dict
|
||||
del self.modules[name]
|
||||
|
|
@ -468,28 +511,28 @@ class Bot(threading.Thread):
|
|||
def quit(self):
|
||||
"""Save and unload modules and disconnect servers"""
|
||||
|
||||
self.datastore.close()
|
||||
|
||||
if self.event_timer is not None:
|
||||
logger.info("Stop the event timer...")
|
||||
self.event_timer.cancel()
|
||||
|
||||
logger.info("Stop consumers")
|
||||
k = self.cnsr_thrd
|
||||
for cnsr in k:
|
||||
cnsr.stop = True
|
||||
|
||||
logger.info("Save and unload all modules...")
|
||||
k = list(self.modules.keys())
|
||||
for mod in k:
|
||||
for mod in [m for m in self.modules.keys()]:
|
||||
self.unload_module(mod)
|
||||
|
||||
logger.info("Close all servers connection...")
|
||||
k = list(self.servers.keys())
|
||||
for srv in k:
|
||||
self.servers[srv].close()
|
||||
for srv in [self.servers[k] for k in self.servers]:
|
||||
srv.close()
|
||||
|
||||
self.stop = True
|
||||
logger.info("Stop consumers")
|
||||
with self.cnsr_lock:
|
||||
k = list(self.cnsr_thrd)
|
||||
for cnsr in k:
|
||||
cnsr.stop = True
|
||||
|
||||
if self.stop is False or sync_queue is not None:
|
||||
self.stop = True
|
||||
sync_act("end")
|
||||
sync_queue.join()
|
||||
|
||||
|
||||
# Treatment
|
||||
|
|
@ -503,20 +546,3 @@ class Bot(threading.Thread):
|
|||
del store[hook.name]
|
||||
elif isinstance(store, list):
|
||||
store.remove(hook)
|
||||
|
||||
|
||||
def hotswap(bak):
|
||||
bak.stop = True
|
||||
if bak.event_timer is not None:
|
||||
bak.event_timer.cancel()
|
||||
bak.datastore.close()
|
||||
|
||||
new = Bot(str(bak.ip), bak.modules_paths, bak.datastore)
|
||||
new.servers = bak.servers
|
||||
new.modules = bak.modules
|
||||
new.modules_configuration = bak.modules_configuration
|
||||
new.events = bak.events
|
||||
new.hooks = bak.hooks
|
||||
|
||||
new._update_event_timer()
|
||||
return new
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
@ -23,16 +21,18 @@ class Channel:
|
|||
|
||||
"""A chat room"""
|
||||
|
||||
def __init__(self, name, password=None):
|
||||
def __init__(self, name, password=None, encoding=None):
|
||||
"""Initialize the channel
|
||||
|
||||
Arguments:
|
||||
name -- the channel name
|
||||
password -- the optional password use to join it
|
||||
encoding -- the optional encoding of the channel
|
||||
"""
|
||||
|
||||
self.name = name
|
||||
self.password = password
|
||||
self.encoding = encoding
|
||||
self.people = dict()
|
||||
self.topic = ""
|
||||
self.logger = logging.getLogger("nemubot.channel." + name)
|
||||
|
|
@ -52,11 +52,11 @@ class Channel:
|
|||
elif cmd == "MODE":
|
||||
self.mode(msg)
|
||||
elif cmd == "JOIN":
|
||||
self.join(msg.nick)
|
||||
self.join(msg.frm)
|
||||
elif cmd == "NICK":
|
||||
self.nick(msg.nick, msg.text)
|
||||
self.nick(msg.frm, msg.text)
|
||||
elif cmd == "PART" or cmd == "QUIT":
|
||||
self.part(msg.nick)
|
||||
self.part(msg.frm)
|
||||
elif cmd == "TOPIC":
|
||||
self.topic = self.text
|
||||
|
||||
|
|
@ -120,17 +120,17 @@ class Channel:
|
|||
else:
|
||||
self.password = msg.text[1]
|
||||
elif msg.text[0] == "+o":
|
||||
self.people[msg.nick] |= 4
|
||||
self.people[msg.frm] |= 4
|
||||
elif msg.text[0] == "-o":
|
||||
self.people[msg.nick] &= ~4
|
||||
self.people[msg.frm] &= ~4
|
||||
elif msg.text[0] == "+h":
|
||||
self.people[msg.nick] |= 2
|
||||
self.people[msg.frm] |= 2
|
||||
elif msg.text[0] == "-h":
|
||||
self.people[msg.nick] &= ~2
|
||||
self.people[msg.frm] &= ~2
|
||||
elif msg.text[0] == "+v":
|
||||
self.people[msg.nick] |= 1
|
||||
self.people[msg.frm] |= 1
|
||||
elif msg.text[0] == "-v":
|
||||
self.people[msg.nick] &= ~1
|
||||
self.people[msg.frm] &= ~1
|
||||
|
||||
def parse332(self, msg):
|
||||
"""Parse RPL_TOPIC message
|
||||
|
|
|
|||
26
nemubot/config/__init__.py
Normal file
26
nemubot/config/__init__.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 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/>.
|
||||
|
||||
def get_boolean(s):
|
||||
if isinstance(s, bool):
|
||||
return s
|
||||
else:
|
||||
return (s and s != "0" and s.lower() != "false" and s.lower() != "off")
|
||||
|
||||
from nemubot.config.include import Include
|
||||
from nemubot.config.module import Module
|
||||
from nemubot.config.nemubot import Nemubot
|
||||
from nemubot.config.server import Server
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
@ -16,8 +14,7 @@
|
|||
# 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 PromptReset(Exception):
|
||||
class Include:
|
||||
|
||||
def __init__(self, type):
|
||||
super(PromptReset, self).__init__("Prompt reset asked")
|
||||
self.type = type
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
26
nemubot/config/module.py
Normal file
26
nemubot/config/module.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 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/>.
|
||||
|
||||
from nemubot.config import get_boolean
|
||||
from nemubot.tools.xmlparser.genericnode import GenericNode
|
||||
|
||||
|
||||
class Module(GenericNode):
|
||||
|
||||
def __init__(self, name, autoload=True, **kwargs):
|
||||
super().__init__(None, **kwargs)
|
||||
self.name = name
|
||||
self.autoload = get_boolean(autoload)
|
||||
46
nemubot/config/nemubot.py
Normal file
46
nemubot/config/nemubot.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# 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/>.
|
||||
|
||||
from nemubot.config.include import Include
|
||||
from nemubot.config.module import Module
|
||||
from nemubot.config.server import Server
|
||||
|
||||
|
||||
class Nemubot:
|
||||
|
||||
def __init__(self, nick="nemubot", realname="nemubot", owner=None,
|
||||
ip=None, ssl=False, caps=None, encoding="utf-8"):
|
||||
self.nick = nick
|
||||
self.realname = realname
|
||||
self.owner = owner
|
||||
self.ip = ip
|
||||
self.caps = caps.split(" ") if caps is not None else []
|
||||
self.encoding = encoding
|
||||
self.servers = []
|
||||
self.modules = []
|
||||
self.includes = []
|
||||
|
||||
|
||||
def addChild(self, name, child):
|
||||
if name == "module" and isinstance(child, Module):
|
||||
self.modules.append(child)
|
||||
return True
|
||||
elif name == "server" and isinstance(child, Server):
|
||||
self.servers.append(child)
|
||||
return True
|
||||
elif name == "include" and isinstance(child, Include):
|
||||
self.includes.append(child)
|
||||
return True
|
||||
45
nemubot/config/server.py
Normal file
45
nemubot/config/server.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# 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/>.
|
||||
|
||||
from nemubot.channel import Channel
|
||||
|
||||
|
||||
class Server:
|
||||
|
||||
def __init__(self, uri="irc://nemubot@localhost/", autoconnect=True, caps=None, **kwargs):
|
||||
self.uri = uri
|
||||
self.autoconnect = autoconnect
|
||||
self.caps = caps.split(" ") if caps is not None else []
|
||||
self.args = kwargs
|
||||
self.channels = []
|
||||
|
||||
|
||||
def addChild(self, name, child):
|
||||
if name == "channel" and isinstance(child, Channel):
|
||||
self.channels.append(child)
|
||||
return True
|
||||
|
||||
|
||||
def server(self, parent, trynb=0):
|
||||
from nemubot.server import factory
|
||||
|
||||
for a in ["nick", "owner", "realname", "encoding"]:
|
||||
if a not in self.args:
|
||||
self.args[a] = getattr(parent, a)
|
||||
|
||||
self.caps += parent.caps
|
||||
|
||||
return factory(self.uri, caps=self.caps, channels=self.channels, trynb=trynb, **self.args)
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
@ -40,42 +38,37 @@ class MessageConsumer:
|
|||
|
||||
msgs = []
|
||||
|
||||
# Parse the message
|
||||
# Parse message
|
||||
try:
|
||||
for msg in self.srv.parse(self.orig):
|
||||
msgs.append(msg)
|
||||
except:
|
||||
logger.exception("Error occurred during the processing of the %s: "
|
||||
"%s", type(self.msgs[0]).__name__, self.msgs[0])
|
||||
"%s", type(self.orig).__name__, self.orig)
|
||||
|
||||
if len(msgs) <= 0:
|
||||
return
|
||||
|
||||
# Qualify the message
|
||||
if not hasattr(msg, "server") or msg.server is None:
|
||||
msg.server = self.srv.id
|
||||
if hasattr(msg, "frm_owner"):
|
||||
msg.frm_owner = (not hasattr(self.srv, "owner") or self.srv.owner == msg.frm)
|
||||
|
||||
# Treat the message
|
||||
# Treat message
|
||||
for msg in msgs:
|
||||
for res in context.treater.treat_msg(msg):
|
||||
# Identify the destination
|
||||
# Identify destination
|
||||
to_server = None
|
||||
if isinstance(res, str):
|
||||
to_server = self.srv
|
||||
elif not hasattr(res, "server"):
|
||||
logger.error("No server defined for response of type %s: %s", type(res).__name__, res)
|
||||
continue
|
||||
elif res.server is None:
|
||||
to_server = self.srv
|
||||
res.server = self.srv.id
|
||||
elif isinstance(res.server, str) and res.server in context.servers:
|
||||
res.server = self.srv.fileno()
|
||||
elif res.server in context.servers:
|
||||
to_server = context.servers[res.server]
|
||||
else:
|
||||
to_server = res.server
|
||||
|
||||
if to_server is None:
|
||||
logger.error("The server defined in this response doesn't "
|
||||
"exist: %s", res.server)
|
||||
if to_server is None or not hasattr(to_server, "send_response") or not callable(to_server.send_response):
|
||||
logger.error("The server defined in this response doesn't exist: %s", res.server)
|
||||
continue
|
||||
|
||||
# Sent the message only if treat_post authorize it
|
||||
# Sent message
|
||||
to_server.send_response(res)
|
||||
|
||||
|
||||
|
|
@ -101,7 +94,7 @@ class EventConsumer:
|
|||
# Or remove reference of this event
|
||||
elif (hasattr(self.evt, "module_src") and
|
||||
self.evt.module_src is not None):
|
||||
self.evt.module_src.__nemubot_context__.events.remove(self.evt.id)
|
||||
self.evt.module_src.__nemubot_context__.events.remove((self.evt, self.evt.id))
|
||||
|
||||
|
||||
|
||||
|
|
@ -112,18 +105,25 @@ class Consumer(threading.Thread):
|
|||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.stop = False
|
||||
threading.Thread.__init__(self)
|
||||
super().__init__(name="Nemubot consumer", daemon=True)
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while not self.stop:
|
||||
stm = self.context.cnsr_queue.get(True, 1)
|
||||
stm.run(self.context)
|
||||
self.context.cnsr_queue.task_done()
|
||||
try:
|
||||
stm = self.context.cnsr_queue.get(True, 1)
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
with self.context.cnsr_lock:
|
||||
self.context.cnsr_active += 1
|
||||
try:
|
||||
stm.run(self.context)
|
||||
finally:
|
||||
self.context.cnsr_queue.task_done()
|
||||
with self.context.cnsr_lock:
|
||||
self.context.cnsr_active -= 1
|
||||
finally:
|
||||
self.context.cnsr_thrd_size -= 2
|
||||
self.context.cnsr_thrd.remove(self)
|
||||
with self.context.cnsr_lock:
|
||||
self.context.cnsr_thrd.remove(self)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
@ -32,16 +32,20 @@ class Abstract:
|
|||
def close(self):
|
||||
return
|
||||
|
||||
def load(self, module):
|
||||
def load(self, module, knodes):
|
||||
"""Load data for the given module
|
||||
|
||||
Argument:
|
||||
module -- the module name of data to load
|
||||
knodes -- the schema to use to load the datas
|
||||
|
||||
Return:
|
||||
The loaded data
|
||||
"""
|
||||
|
||||
if knodes is not None:
|
||||
return None
|
||||
|
||||
return self.new()
|
||||
|
||||
def save(self, module, data):
|
||||
|
|
|
|||
|
|
@ -83,27 +83,38 @@ class XML(Abstract):
|
|||
|
||||
return os.path.join(self.basedir, module + ".xml")
|
||||
|
||||
def load(self, module):
|
||||
def load(self, module, knodes):
|
||||
"""Load data for the given module
|
||||
|
||||
Argument:
|
||||
module -- the module name of data to load
|
||||
knodes -- the schema to use to load the datas
|
||||
"""
|
||||
|
||||
data_file = self._get_data_file_path(module)
|
||||
|
||||
if knodes is None:
|
||||
from nemubot.tools.xmlparser import parse_file
|
||||
def _true_load(path):
|
||||
return parse_file(path)
|
||||
|
||||
else:
|
||||
from nemubot.tools.xmlparser import XMLParser
|
||||
p = XMLParser(knodes)
|
||||
def _true_load(path):
|
||||
return p.parse_file(path)
|
||||
|
||||
# Try to load original file
|
||||
if os.path.isfile(data_file):
|
||||
from nemubot.tools.xmlparser import parse_file
|
||||
try:
|
||||
return parse_file(data_file)
|
||||
return _true_load(data_file)
|
||||
except xml.parsers.expat.ExpatError:
|
||||
# Try to load from backup
|
||||
for i in range(10):
|
||||
path = data_file + "." + str(i)
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
cnt = parse_file(path)
|
||||
cnt = _true_load(path)
|
||||
|
||||
logger.warn("Restoring from backup: %s", path)
|
||||
|
||||
|
|
@ -112,7 +123,7 @@ class XML(Abstract):
|
|||
continue
|
||||
|
||||
# Default case: initialize a new empty datastore
|
||||
return Abstract.load(self, module)
|
||||
return super().load(module, knodes)
|
||||
|
||||
def _rotate(self, path):
|
||||
"""Backup given path
|
||||
|
|
@ -143,4 +154,18 @@ class XML(Abstract):
|
|||
if self.rotate:
|
||||
self._rotate(path)
|
||||
|
||||
return data.save(path)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
import tempfile
|
||||
_, tmpath = tempfile.mkstemp()
|
||||
with open(tmpath, "w") as f:
|
||||
import xml.sax.saxutils
|
||||
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
|
||||
gen.startDocument()
|
||||
data.saveElement(gen)
|
||||
gen.endDocument()
|
||||
|
||||
# Atomic save
|
||||
import shutil
|
||||
shutil.move(tmpath, path)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
#
|
||||
|
|
@ -23,18 +21,14 @@ class ModuleEvent:
|
|||
|
||||
"""Representation of a event initiated by a bot module"""
|
||||
|
||||
def __init__(self, call=None, call_data=None, func=None, func_data=None,
|
||||
cmp=None, cmp_data=None, interval=60, offset=0, times=1):
|
||||
def __init__(self, call=None, func=None, cmp=None, interval=60, offset=0, times=1):
|
||||
|
||||
"""Initialize the event
|
||||
|
||||
Keyword arguments:
|
||||
call -- Function to call when the event is realized
|
||||
call_data -- Argument(s) (single or dict) to pass as argument
|
||||
func -- Function called to check
|
||||
func_data -- Argument(s) (single or dict) to pass as argument OR if no func, initial data to watch
|
||||
cmp -- Boolean function called to check changes
|
||||
cmp_data -- Argument(s) (single or dict) to pass as argument OR if no cmp, data compared to previous
|
||||
cmp -- Boolean function called to check changes or value to compare with
|
||||
interval -- Time in seconds between each check (default: 60)
|
||||
offset -- Time in seconds added to interval before the first check (default: 0)
|
||||
times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1)
|
||||
|
|
@ -42,31 +36,22 @@ class ModuleEvent:
|
|||
|
||||
# What have we to check?
|
||||
self.func = func
|
||||
self.func_data = func_data
|
||||
|
||||
# How detect a change?
|
||||
self.cmp = cmp
|
||||
self.cmp_data = None
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# Store times
|
||||
self.offset = timedelta(seconds=offset) # Time to wait before the first check
|
||||
self.interval = timedelta(seconds=interval)
|
||||
if isinstance(offset, timedelta):
|
||||
self.offset = offset # Time to wait before the first check
|
||||
else:
|
||||
self.offset = timedelta(seconds=offset) # Time to wait before the first check
|
||||
if isinstance(interval, timedelta):
|
||||
self.interval = interval
|
||||
else:
|
||||
self.interval = timedelta(seconds=interval)
|
||||
self._end = None # Cache
|
||||
|
||||
# How many times do this event?
|
||||
|
|
@ -97,44 +82,23 @@ class ModuleEvent:
|
|||
"""Return the time left before/after the near check"""
|
||||
if self.current is not None:
|
||||
return self.current - datetime.now(timezone.utc)
|
||||
return 99999 # TODO: 99999 is not a valid time to return
|
||||
return timedelta.max
|
||||
|
||||
def check(self):
|
||||
"""Run a check and realized the event if this is time"""
|
||||
|
||||
# Get initial data
|
||||
if self.func is None:
|
||||
d_init = self.func_data
|
||||
elif self.func_data is None:
|
||||
d_init = self.func()
|
||||
elif isinstance(self.func_data, dict):
|
||||
d_init = self.func(**self.func_data)
|
||||
# Get new data
|
||||
if self.func is not None:
|
||||
d_new = self.func()
|
||||
else:
|
||||
d_init = self.func(self.func_data)
|
||||
d_new = None
|
||||
|
||||
# then compare with current data
|
||||
if self.cmp is None:
|
||||
if self.cmp_data is None:
|
||||
rlz = True
|
||||
else:
|
||||
rlz = (d_init != self.cmp_data)
|
||||
elif self.cmp_data is None:
|
||||
rlz = self.cmp(d_init)
|
||||
elif isinstance(self.cmp_data, dict):
|
||||
rlz = self.cmp(d_init, **self.cmp_data)
|
||||
else:
|
||||
rlz = self.cmp(d_init, self.cmp_data)
|
||||
|
||||
if rlz:
|
||||
if self.cmp is None or (callable(self.cmp) and self.cmp(d_new)) or (not callable(self.cmp) and d_new != self.cmp):
|
||||
self.times -= 1
|
||||
|
||||
# Call attended function
|
||||
if self.call_data is None:
|
||||
if d_init is None:
|
||||
self.call()
|
||||
else:
|
||||
self.call(d_init)
|
||||
elif isinstance(self.call_data, dict):
|
||||
self.call(d_init, **self.call_data)
|
||||
if self.func is not None:
|
||||
self.call(d_new)
|
||||
else:
|
||||
self.call(d_init, self.call_data)
|
||||
self.call()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
#
|
||||
|
|
@ -16,20 +14,21 @@
|
|||
# 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 IRCException(Exception):
|
||||
class IMException(Exception):
|
||||
|
||||
|
||||
def __init__(self, message, personnal=True):
|
||||
super(IRCException, self).__init__(message)
|
||||
self.message = message
|
||||
super(IMException, self).__init__(message)
|
||||
self.personnal = personnal
|
||||
|
||||
|
||||
def fill_response(self, msg):
|
||||
if self.personnal:
|
||||
from nemubot.message import DirectAsk
|
||||
return DirectAsk(msg.frm, self.message,
|
||||
return DirectAsk(msg.frm, *self.args,
|
||||
server=msg.server, to=msg.to_response)
|
||||
|
||||
else:
|
||||
from nemubot.message import Text
|
||||
return Text(self.message,
|
||||
return Text(*self.args,
|
||||
server=msg.server, to=msg.to_response)
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
#
|
||||
|
|
@ -16,8 +14,10 @@
|
|||
# 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 PromptError(Exception):
|
||||
from nemubot.exception import IMException
|
||||
|
||||
|
||||
class KeywordException(IMException):
|
||||
|
||||
def __init__(self, message):
|
||||
super(PromptError, self).__init__(message)
|
||||
self.message = message
|
||||
super(KeywordException, self).__init__(message)
|
||||
|
|
@ -15,29 +15,37 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from nemubot.hooks.abstract import Abstract
|
||||
from nemubot.hooks.command import Command
|
||||
from nemubot.hooks.message import Message
|
||||
|
||||
last_registered = []
|
||||
|
||||
class hook:
|
||||
|
||||
last_registered = []
|
||||
|
||||
|
||||
def hook(store, *args, **kargs):
|
||||
"""Function used as a decorator for module loading"""
|
||||
def sec(call):
|
||||
last_registered.append((store, Message(call, *args, **kargs)))
|
||||
return call
|
||||
return sec
|
||||
def _add(store, h, *args, **kwargs):
|
||||
"""Function used as a decorator for module loading"""
|
||||
def sec(call):
|
||||
hook.last_registered.append((store, h(call, *args, **kwargs)))
|
||||
return call
|
||||
return sec
|
||||
|
||||
|
||||
def reload():
|
||||
global Abstract, Message
|
||||
import imp
|
||||
def add(store, *args, **kwargs):
|
||||
return hook._add(store, Abstract, *args, **kwargs)
|
||||
|
||||
import nemubot.hooks.abstract
|
||||
imp.reload(nemubot.hooks.abstract)
|
||||
Abstract = nemubot.hooks.abstract.Abstract
|
||||
import nemubot.hooks.message
|
||||
imp.reload(nemubot.hooks.message)
|
||||
Message = nemubot.hooks.message.Message
|
||||
def ask(*args, store=["in","DirectAsk"], **kwargs):
|
||||
return hook._add(store, Message, *args, **kwargs)
|
||||
|
||||
import nemubot.hooks.manager
|
||||
imp.reload(nemubot.hooks.manager)
|
||||
def command(*args, store=["in","Command"], **kwargs):
|
||||
return hook._add(store, Command, *args, **kwargs)
|
||||
|
||||
def message(*args, store=["in","Text"], **kwargs):
|
||||
return hook._add(store, Message, *args, **kwargs)
|
||||
|
||||
def post(*args, store=["post"], **kwargs):
|
||||
return hook._add(store, Abstract, *args, **kwargs)
|
||||
|
||||
def pre(*args, store=["pre"], **kwargs):
|
||||
return hook._add(store, Abstract, *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
# 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 types
|
||||
|
||||
def call_game(call, *args, **kargs):
|
||||
"""With given args, try to determine the right call to make
|
||||
|
||||
|
|
@ -42,30 +44,95 @@ class Abstract:
|
|||
|
||||
"""Abstract class for Hook implementation"""
|
||||
|
||||
def __init__(self, call, data=None, mtimes=-1, end_call=None):
|
||||
def __init__(self, call, data=None, channels=None, servers=None, mtimes=-1,
|
||||
end_call=None, check=None, match=None):
|
||||
"""Create basis of the hook
|
||||
|
||||
Arguments:
|
||||
call -- function to call to perform the hook
|
||||
|
||||
Keyword arguments:
|
||||
data -- optional datas passed to call
|
||||
"""
|
||||
|
||||
if channels is None: channels = list()
|
||||
if servers is None: servers = list()
|
||||
|
||||
assert callable(call), call
|
||||
assert end_call is None or callable(end_call), end_call
|
||||
assert check is None or callable(check), check
|
||||
assert match is None or callable(match), match
|
||||
assert isinstance(channels, list), channels
|
||||
assert isinstance(servers, list), servers
|
||||
assert type(mtimes) is int, mtimes
|
||||
|
||||
self.call = call
|
||||
self.data = data
|
||||
|
||||
self.mod_check = check
|
||||
self.mod_match = match
|
||||
|
||||
# TODO: find a way to have only one list: a limit is server + channel, not only server or channel
|
||||
self.channels = channels
|
||||
self.servers = servers
|
||||
|
||||
self.times = mtimes
|
||||
self.end_call = end_call
|
||||
|
||||
|
||||
def match(self, data1, server):
|
||||
return NotImplemented
|
||||
def can_read(self, receivers=list(), server=None):
|
||||
assert isinstance(receivers, list), receivers
|
||||
|
||||
if server is None or len(self.servers) == 0 or server in self.servers:
|
||||
if len(self.channels) == 0:
|
||||
return True
|
||||
|
||||
for receiver in receivers:
|
||||
if receiver in self.channels:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return ""
|
||||
|
||||
|
||||
def can_write(self, receivers=list(), server=None):
|
||||
return True
|
||||
|
||||
|
||||
def check(self, data1):
|
||||
return self.mod_check(data1) if self.mod_check is not None else True
|
||||
|
||||
|
||||
def match(self, data1):
|
||||
return self.mod_match(data1) if self.mod_match is not None else True
|
||||
|
||||
|
||||
def run(self, data1, *args):
|
||||
"""Run the hook"""
|
||||
|
||||
from nemubot.exception import IRCException
|
||||
from nemubot.exception import IMException
|
||||
self.times -= 1
|
||||
|
||||
ret = None
|
||||
|
||||
try:
|
||||
ret = call_game(self.call, data1, self.data, *args)
|
||||
except IRCException as e:
|
||||
if self.check(data1):
|
||||
ret = call_game(self.call, data1, self.data, *args)
|
||||
if isinstance(ret, types.GeneratorType):
|
||||
for r in ret:
|
||||
yield r
|
||||
ret = None
|
||||
except IMException as e:
|
||||
ret = e.fill_response(data1)
|
||||
finally:
|
||||
if self.times == 0:
|
||||
self.call_end(ret)
|
||||
|
||||
return ret
|
||||
if isinstance(ret, list):
|
||||
for r in ret:
|
||||
yield ret
|
||||
elif ret is not None:
|
||||
yield ret
|
||||
|
|
|
|||
67
nemubot/hooks/command.py
Normal file
67
nemubot/hooks/command.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 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 nemubot.hooks.message import Message
|
||||
from nemubot.hooks.abstract import Abstract
|
||||
from nemubot.hooks.keywords import NoKeyword
|
||||
from nemubot.hooks.keywords.abstract import Abstract as AbstractKeywords
|
||||
from nemubot.hooks.keywords.dict import Dict as DictKeywords
|
||||
import nemubot.message
|
||||
|
||||
|
||||
class Command(Message):
|
||||
|
||||
"""Class storing hook information, specialized for Command messages"""
|
||||
|
||||
def __init__(self, call, name=None, help_usage=dict(), keywords=NoKeyword(),
|
||||
**kargs):
|
||||
|
||||
super().__init__(call=call, **kargs)
|
||||
|
||||
if isinstance(keywords, dict):
|
||||
keywords = DictKeywords(keywords)
|
||||
|
||||
assert type(help_usage) is dict, help_usage
|
||||
assert isinstance(keywords, AbstractKeywords), keywords
|
||||
|
||||
self.name = str(name) if name is not None else None
|
||||
self.help_usage = help_usage
|
||||
self.keywords = keywords
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "\x03\x02%s\x03\x02%s%s" % (
|
||||
self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "",
|
||||
" (restricted to %:%s)" % ((",".join(self.servers) if self.server else "*") + (",".join(self.channels) if self.channels else "*")) if len(self.channels) or len(self.servers) else "",
|
||||
": %s" % self.help if self.help is not None else ""
|
||||
)
|
||||
|
||||
|
||||
def check(self, msg):
|
||||
return self.keywords.check(msg.kwargs) and super().check(msg)
|
||||
|
||||
|
||||
def match(self, msg):
|
||||
if not isinstance(msg, nemubot.message.command.Command):
|
||||
return False
|
||||
else:
|
||||
return (
|
||||
(self.name is None or msg.cmd == self.name) and
|
||||
(self.regexp is None or re.match(self.regexp, msg.cmd)) and
|
||||
Abstract.match(self, msg)
|
||||
)
|
||||
47
nemubot/hooks/keywords/__init__.py
Normal file
47
nemubot/hooks/keywords/__init__.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# 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/>.
|
||||
|
||||
from nemubot.exception.keyword import KeywordException
|
||||
from nemubot.hooks.keywords.abstract import Abstract
|
||||
|
||||
|
||||
class NoKeyword(Abstract):
|
||||
|
||||
def check(self, mkw):
|
||||
if len(mkw):
|
||||
raise KeywordException("This command doesn't take any keyword arguments.")
|
||||
return super().check(mkw)
|
||||
|
||||
|
||||
class AnyKeyword(Abstract):
|
||||
|
||||
def __init__(self, h):
|
||||
"""Class that accepts any passed keywords
|
||||
|
||||
Arguments:
|
||||
h -- Help string
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.h = h
|
||||
|
||||
|
||||
def check(self, mkw):
|
||||
return super().check(mkw)
|
||||
|
||||
|
||||
def help(self):
|
||||
return self.h
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# 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
|
||||
|
|
@ -18,18 +16,20 @@
|
|||
|
||||
class Abstract:
|
||||
|
||||
def to_bot_message(self, srv):
|
||||
"""Convert to one of concrete implementation of AbstractMessage
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def check(self, mkw):
|
||||
"""Check that all given message keywords are valid
|
||||
|
||||
Argument:
|
||||
srv -- the server from the message was received
|
||||
mkw -- dictionnary of keywords present in the message
|
||||
"""
|
||||
|
||||
raise NotImplemented
|
||||
assert type(mkw) is dict, mkw
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def to_server_string(self, **kwargs):
|
||||
"""Pretty print the message to close to original input string
|
||||
"""
|
||||
|
||||
raise NotImplemented
|
||||
def help(self):
|
||||
return ""
|
||||
59
nemubot/hooks/keywords/dict.py
Normal file
59
nemubot/hooks/keywords/dict.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# 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/>.
|
||||
|
||||
from nemubot.exception.keyword import KeywordException
|
||||
from nemubot.hooks.keywords.abstract import Abstract
|
||||
from nemubot.tools.human import guess
|
||||
|
||||
|
||||
class Dict(Abstract):
|
||||
|
||||
|
||||
def __init__(self, d):
|
||||
super().__init__()
|
||||
self.d = d
|
||||
|
||||
|
||||
@property
|
||||
def chk_noarg(self):
|
||||
if not hasattr(self, "_cache_chk_noarg"):
|
||||
self._cache_chk_noarg = [k for k in self.d if "=" not in k]
|
||||
return self._cache_chk_noarg
|
||||
|
||||
|
||||
@property
|
||||
def chk_args(self):
|
||||
if not hasattr(self, "_cache_chk_args"):
|
||||
self._cache_chk_args = [k.split("=", 1)[0] for k in self.d if "=" in k]
|
||||
return self._cache_chk_args
|
||||
|
||||
|
||||
def check(self, mkw):
|
||||
for k in mkw:
|
||||
if ((k + "?") not in self.chk_args) and ((mkw[k] and k not in self.chk_args) or (not mkw[k] and k not in self.chk_noarg)):
|
||||
if mkw[k] and k in self.chk_noarg:
|
||||
raise KeywordException("Keyword %s doesn't take value." % k)
|
||||
elif not mkw[k] and k in self.chk_args:
|
||||
raise KeywordException("Keyword %s requires a value." % k)
|
||||
else:
|
||||
ch = [c for c in guess(k, self.d)]
|
||||
raise KeywordException("Unknown keyword %s." % k + (" Did you mean: " + ", ".join(ch) + "?" if len(ch) else ""))
|
||||
|
||||
return super().check(mkw)
|
||||
|
||||
|
||||
def help(self):
|
||||
return ["\x03\x02@%s\x03\x02: %s" % (k, self.d[k]) for k in self.d]
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
#
|
||||
|
|
@ -16,15 +14,47 @@
|
|||
# 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 logging
|
||||
|
||||
|
||||
class HooksManager:
|
||||
|
||||
"""Class to manage hooks"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, name="core"):
|
||||
"""Initialize the manager"""
|
||||
|
||||
self.hooks = dict()
|
||||
self.logger = logging.getLogger("nemubot.hooks.manager." + name)
|
||||
|
||||
|
||||
def _access(self, *triggers):
|
||||
"""Access to the given triggers chain"""
|
||||
|
||||
h = self.hooks
|
||||
for t in triggers:
|
||||
if t not in h:
|
||||
h[t] = dict()
|
||||
h = h[t]
|
||||
|
||||
if "__end__" not in h:
|
||||
h["__end__"] = list()
|
||||
|
||||
return h
|
||||
|
||||
|
||||
def _search(self, hook, *where, start=None):
|
||||
"""Search all occurence of the given hook"""
|
||||
|
||||
if start is None:
|
||||
start = self.hooks
|
||||
|
||||
for k in start:
|
||||
if k == "__end__":
|
||||
if hook in start[k]:
|
||||
yield where
|
||||
else:
|
||||
yield from self._search(hook, *where + (k,), start=start[k])
|
||||
|
||||
|
||||
def add_hook(self, hook, *triggers):
|
||||
|
|
@ -35,20 +65,19 @@ class HooksManager:
|
|||
triggers -- string that trigger the hook
|
||||
"""
|
||||
|
||||
trigger = "_".join(triggers)
|
||||
assert hook is not None, hook
|
||||
|
||||
if trigger not in self.hooks:
|
||||
self.hooks[trigger] = list()
|
||||
h = self._access(*triggers)
|
||||
|
||||
self.hooks[trigger].append(hook)
|
||||
h["__end__"].append(hook)
|
||||
|
||||
self.logger.debug("New hook successfully added in %s: %s",
|
||||
"/".join(triggers), hook)
|
||||
|
||||
|
||||
def del_hook(self, hook=None, *triggers):
|
||||
def del_hooks(self, *triggers, hook=None):
|
||||
"""Remove the given hook from the manager
|
||||
|
||||
Return:
|
||||
Boolean value reporting the deletion success
|
||||
|
||||
Argument:
|
||||
triggers -- trigger string to remove
|
||||
|
||||
|
|
@ -56,15 +85,20 @@ class HooksManager:
|
|||
hook -- a Hook instance to remove from the trigger string
|
||||
"""
|
||||
|
||||
trigger = "_".join(triggers)
|
||||
assert hook is not None or len(triggers)
|
||||
|
||||
if trigger in self.hooks:
|
||||
if hook is None:
|
||||
del self.hooks[trigger]
|
||||
self.logger.debug("Trying to delete hook in %s: %s",
|
||||
"/".join(triggers), hook)
|
||||
|
||||
if hook is not None:
|
||||
for h in self._search(hook, *triggers, start=self._access(*triggers)):
|
||||
self._access(*h)["__end__"].remove(hook)
|
||||
|
||||
else:
|
||||
if len(triggers):
|
||||
del self._access(*triggers[:-1])[triggers[-1]]
|
||||
else:
|
||||
self.hooks[trigger].remove(hook)
|
||||
return True
|
||||
return False
|
||||
self.hooks = dict()
|
||||
|
||||
|
||||
def get_hooks(self, *triggers):
|
||||
|
|
@ -72,35 +106,29 @@ class HooksManager:
|
|||
|
||||
Argument:
|
||||
triggers -- the trigger string
|
||||
|
||||
Keyword argument:
|
||||
data -- Data to pass to the hook as argument
|
||||
"""
|
||||
|
||||
trigger = "_".join(triggers)
|
||||
|
||||
res = list()
|
||||
|
||||
for key in self.hooks:
|
||||
if trigger.find(key) == 0:
|
||||
res += self.hooks[key]
|
||||
|
||||
return res
|
||||
for n in range(len(triggers) + 1):
|
||||
i = self._access(*triggers[:n])
|
||||
for h in i["__end__"]:
|
||||
yield h
|
||||
|
||||
|
||||
def exec_hook(self, *triggers, **data):
|
||||
"""Trigger hooks that match the given trigger string
|
||||
def get_reverse_hooks(self, *triggers, exclude_first=False):
|
||||
"""Returns list of triggered hooks that are bellow or at the same level
|
||||
|
||||
Argument:
|
||||
trigger -- the trigger string
|
||||
triggers -- the trigger string
|
||||
|
||||
Keyword argument:
|
||||
data -- Data to pass to the hook as argument
|
||||
Keyword arguments:
|
||||
exclude_first -- start reporting hook at the next level
|
||||
"""
|
||||
|
||||
trigger = "_".join(triggers)
|
||||
|
||||
for key in self.hooks:
|
||||
if trigger.find(key) == 0:
|
||||
for hook in self.hooks[key]:
|
||||
hook.run(**data)
|
||||
h = self._access(*triggers)
|
||||
for k in h:
|
||||
if k == "__end__":
|
||||
if not exclude_first:
|
||||
for hk in h[k]:
|
||||
yield hk
|
||||
else:
|
||||
yield from self.get_reverse_hooks(*triggers + (k,))
|
||||
|
|
|
|||
115
nemubot/hooks/manager_test.py
Executable file
115
nemubot/hooks/manager_test.py
Executable file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import unittest
|
||||
|
||||
from nemubot.hooks.manager import HooksManager
|
||||
|
||||
class TestHookManager(unittest.TestCase):
|
||||
|
||||
|
||||
def test_access(self):
|
||||
hm = HooksManager()
|
||||
|
||||
h1 = "HOOK1"
|
||||
h2 = "HOOK2"
|
||||
h3 = "HOOK3"
|
||||
|
||||
hm.add_hook(h1)
|
||||
hm.add_hook(h2, "pre")
|
||||
hm.add_hook(h3, "pre", "Text")
|
||||
hm.add_hook(h2, "post", "Text")
|
||||
|
||||
self.assertIn("__end__", hm._access())
|
||||
self.assertIn("__end__", hm._access("pre"))
|
||||
self.assertIn("__end__", hm._access("pre", "Text"))
|
||||
self.assertIn("__end__", hm._access("post", "Text"))
|
||||
|
||||
self.assertFalse(hm._access("inexistant")["__end__"])
|
||||
self.assertTrue(hm._access()["__end__"])
|
||||
self.assertTrue(hm._access("pre")["__end__"])
|
||||
self.assertTrue(hm._access("pre", "Text")["__end__"])
|
||||
self.assertTrue(hm._access("post", "Text")["__end__"])
|
||||
|
||||
|
||||
def test_search(self):
|
||||
hm = HooksManager()
|
||||
|
||||
h1 = "HOOK1"
|
||||
h2 = "HOOK2"
|
||||
h3 = "HOOK3"
|
||||
h4 = "HOOK4"
|
||||
|
||||
hm.add_hook(h1)
|
||||
hm.add_hook(h2, "pre")
|
||||
hm.add_hook(h3, "pre", "Text")
|
||||
hm.add_hook(h2, "post", "Text")
|
||||
|
||||
self.assertTrue([h for h in hm._search(h1)])
|
||||
self.assertFalse([h for h in hm._search(h4)])
|
||||
self.assertEqual(2, len([h for h in hm._search(h2)]))
|
||||
self.assertEqual([("pre", "Text")], [h for h in hm._search(h3)])
|
||||
|
||||
|
||||
def test_delete(self):
|
||||
hm = HooksManager()
|
||||
|
||||
h1 = "HOOK1"
|
||||
h2 = "HOOK2"
|
||||
h3 = "HOOK3"
|
||||
h4 = "HOOK4"
|
||||
|
||||
hm.add_hook(h1)
|
||||
hm.add_hook(h2, "pre")
|
||||
hm.add_hook(h3, "pre", "Text")
|
||||
hm.add_hook(h2, "post", "Text")
|
||||
|
||||
hm.del_hooks(hook=h4)
|
||||
|
||||
self.assertTrue(hm._access("pre")["__end__"])
|
||||
self.assertTrue(hm._access("pre", "Text")["__end__"])
|
||||
hm.del_hooks("pre")
|
||||
self.assertFalse(hm._access("pre")["__end__"])
|
||||
|
||||
self.assertTrue(hm._access("post", "Text")["__end__"])
|
||||
hm.del_hooks("post", "Text", hook=h2)
|
||||
self.assertFalse(hm._access("post", "Text")["__end__"])
|
||||
|
||||
self.assertTrue(hm._access()["__end__"])
|
||||
hm.del_hooks(hook=h1)
|
||||
self.assertFalse(hm._access()["__end__"])
|
||||
|
||||
|
||||
def test_get(self):
|
||||
hm = HooksManager()
|
||||
|
||||
h1 = "HOOK1"
|
||||
h2 = "HOOK2"
|
||||
h3 = "HOOK3"
|
||||
|
||||
hm.add_hook(h1)
|
||||
hm.add_hook(h2, "pre")
|
||||
hm.add_hook(h3, "pre", "Text")
|
||||
hm.add_hook(h2, "post", "Text")
|
||||
|
||||
self.assertEqual([h1, h2], [h for h in hm.get_hooks("pre")])
|
||||
self.assertEqual([h1, h2, h3], [h for h in hm.get_hooks("pre", "Text")])
|
||||
|
||||
|
||||
def test_get_rev(self):
|
||||
hm = HooksManager()
|
||||
|
||||
h1 = "HOOK1"
|
||||
h2 = "HOOK2"
|
||||
h3 = "HOOK3"
|
||||
|
||||
hm.add_hook(h1)
|
||||
hm.add_hook(h2, "pre")
|
||||
hm.add_hook(h3, "pre", "Text")
|
||||
hm.add_hook(h2, "post", "Text")
|
||||
|
||||
self.assertEqual([h2, h3], [h for h in hm.get_reverse_hooks("pre")])
|
||||
self.assertEqual([h3], [h for h in hm.get_reverse_hooks("pre", exclude_first=True)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -24,54 +24,26 @@ class Message(Abstract):
|
|||
|
||||
"""Class storing hook information, specialized for a generic Message"""
|
||||
|
||||
def __init__(self, call, name=None, regexp=None, channels=list(),
|
||||
server=None, help=None, help_usage=dict(), **kargs):
|
||||
|
||||
Abstract.__init__(self, call=call, **kargs)
|
||||
def __init__(self, call, regexp=None, help=None, **kwargs):
|
||||
super().__init__(call=call, **kwargs)
|
||||
|
||||
assert regexp is None or type(regexp) is str, regexp
|
||||
assert channels is None or type(channels) is list, channels
|
||||
assert server is None or type(server) is str, server
|
||||
assert type(help_usage) is dict, help_usage
|
||||
|
||||
self.name = str(name) if name is not None else None
|
||||
self.regexp = regexp
|
||||
self.server = server
|
||||
self.channels = channels
|
||||
self.help = help
|
||||
self.help_usage = help_usage
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "\x03\x02%s\x03\x02%s%s" % (
|
||||
self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "",
|
||||
" (restricted to %s)" % (self.server + ":" if self.server is not None else "") + (self.channels if self.channels else "*") if len(self.channels) or self.server else "",
|
||||
": %s" % self.help if self.help is not None else ""
|
||||
)
|
||||
# TODO: find a way to name the feature (like command: help)
|
||||
return self.help if self.help is not None else super().__str__()
|
||||
|
||||
|
||||
def match(self, msg, server=None):
|
||||
if not isinstance(msg, nemubot.message.abstract.Abstract):
|
||||
return True
|
||||
def check(self, msg):
|
||||
return super().check(msg)
|
||||
|
||||
elif isinstance(msg, nemubot.message.Command):
|
||||
return self.is_matching(msg.cmd, msg.to, server)
|
||||
elif isinstance(msg, nemubot.message.Text):
|
||||
return self.is_matching(msg.message, msg.to, server)
|
||||
else:
|
||||
|
||||
def match(self, msg):
|
||||
if not isinstance(msg, nemubot.message.text.Text):
|
||||
return False
|
||||
|
||||
|
||||
def is_matching(self, strcmp, receivers=list(), server=None):
|
||||
"""Test if the current hook correspond to the message"""
|
||||
if ((server is None or self.server is None or self.server == server)
|
||||
and ((self.name is None or strcmp == self.name) and (
|
||||
self.regexp is None or re.match(self.regexp, strcmp)))):
|
||||
|
||||
if receivers and self.channels:
|
||||
for receiver in receivers:
|
||||
if receiver in self.channels:
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return (self.regexp is None or re.match(self.regexp, msg.message)) and super().match(msg)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
#
|
||||
|
|
@ -31,16 +29,16 @@ class ModuleFinder(Finder):
|
|||
self.add_module = add_module
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
# Search only for new nemubot modules (packages init)
|
||||
if path is None:
|
||||
if path is not None and fullname.startswith("nemubot.module."):
|
||||
module_name = fullname.split(".", 2)[2]
|
||||
for mpath in self.modules_paths:
|
||||
if os.path.isfile(os.path.join(mpath, fullname + ".py")):
|
||||
if os.path.isfile(os.path.join(mpath, module_name + ".py")):
|
||||
return ModuleLoader(self.add_module, fullname,
|
||||
os.path.join(mpath, fullname + ".py"))
|
||||
elif os.path.isfile(os.path.join(os.path.join(mpath, fullname), "__init__.py")):
|
||||
os.path.join(mpath, module_name + ".py"))
|
||||
elif os.path.isfile(os.path.join(os.path.join(mpath, module_name), "__init__.py")):
|
||||
return ModuleLoader(self.add_module, fullname,
|
||||
os.path.join(
|
||||
os.path.join(mpath, fullname),
|
||||
os.path.join(mpath, module_name),
|
||||
"__init__.py"))
|
||||
return None
|
||||
|
||||
|
|
@ -55,17 +53,17 @@ class ModuleLoader(SourceFileLoader):
|
|||
def _load(self, module, name):
|
||||
# Add the module to the global modules list
|
||||
self.add_module(module)
|
||||
logger.info("Module '%s' successfully loaded.", name)
|
||||
logger.info("Module '%s' successfully imported from %s.", name.split(".", 2)[2], self.path)
|
||||
return module
|
||||
|
||||
|
||||
# Python 3.4
|
||||
def exec_module(self, module):
|
||||
super(ModuleLoader, self).exec_module(module)
|
||||
super().exec_module(module)
|
||||
self._load(module, module.__spec__.name)
|
||||
|
||||
|
||||
# Python 3.3
|
||||
def load_module(self, fullname):
|
||||
module = super(ModuleLoader, self).load_module(fullname)
|
||||
module = super().load_module(fullname)
|
||||
return self._load(module, module.__name__)
|
||||
|
|
|
|||
|
|
@ -19,27 +19,3 @@ from nemubot.message.text import Text
|
|||
from nemubot.message.directask import DirectAsk
|
||||
from nemubot.message.command import Command
|
||||
from nemubot.message.command import OwnerCommand
|
||||
|
||||
|
||||
def reload():
|
||||
global Abstract, Text, DirectAsk, Command, OwnerCommand
|
||||
import imp
|
||||
|
||||
import nemubot.message.abstract
|
||||
imp.reload(nemubot.message.abstract)
|
||||
Abstract = nemubot.message.abstract.Abstract
|
||||
imp.reload(nemubot.message.text)
|
||||
Text = nemubot.message.text.Text
|
||||
imp.reload(nemubot.message.directask)
|
||||
DirectAsk = nemubot.message.directask.DirectAsk
|
||||
imp.reload(nemubot.message.command)
|
||||
Command = nemubot.message.command.Command
|
||||
OwnerCommand = nemubot.message.command.OwnerCommand
|
||||
|
||||
import nemubot.message.visitor
|
||||
imp.reload(nemubot.message.visitor)
|
||||
|
||||
import nemubot.message.printer
|
||||
imp.reload(nemubot.message.printer)
|
||||
|
||||
nemubot.message.printer.reload()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class Abstract:
|
|||
|
||||
"""This class represents an abstract message"""
|
||||
|
||||
def __init__(self, server=None, date=None, to=None, to_response=None, frm=None):
|
||||
def __init__(self, server=None, date=None, to=None, to_response=None, frm=None, frm_owner=False):
|
||||
"""Initialize an abstract message
|
||||
|
||||
Arguments:
|
||||
|
|
@ -40,7 +40,7 @@ class Abstract:
|
|||
else [ to_response ])
|
||||
self.frm = frm # None allowed when it designate this bot
|
||||
|
||||
self.frm_owner = False # Filled later, in consumer
|
||||
self.frm_owner = frm_owner
|
||||
|
||||
|
||||
@property
|
||||
|
|
@ -51,11 +51,6 @@ class Abstract:
|
|||
return self.to
|
||||
|
||||
|
||||
@property
|
||||
def receivers(self):
|
||||
# TODO: this is for legacy modules
|
||||
return self.to_response
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
# TODO: this is for legacy modules
|
||||
|
|
@ -64,12 +59,6 @@ class Abstract:
|
|||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def nick(self):
|
||||
# TODO: this is for legacy modules
|
||||
return self.frm
|
||||
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit(self)
|
||||
|
||||
|
|
@ -83,7 +72,8 @@ class Abstract:
|
|||
"date": self.date,
|
||||
"to": self.to,
|
||||
"to_response": self._to_response,
|
||||
"frm": self.frm
|
||||
"frm": self.frm,
|
||||
"frm_owner": self.frm_owner,
|
||||
}
|
||||
|
||||
for w in without:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class Command(Abstract):
|
|||
"""This class represents a specialized TextMessage"""
|
||||
|
||||
def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs):
|
||||
Abstract.__init__(self, *nargs, **kargs)
|
||||
super().__init__(*nargs, **kargs)
|
||||
|
||||
self.cmd = cmd
|
||||
self.args = args if args is not None else list()
|
||||
|
|
@ -31,11 +31,6 @@ class Command(Abstract):
|
|||
def __str__(self):
|
||||
return self.cmd + " @" + ",@".join(self.args)
|
||||
|
||||
@property
|
||||
def cmds(self):
|
||||
# TODO: this is for legacy modules
|
||||
return [self.cmd] + self.args
|
||||
|
||||
|
||||
class OwnerCommand(Command):
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class DirectAsk(Text):
|
|||
designated -- the user designated by the message
|
||||
"""
|
||||
|
||||
Text.__init__(self, *args, **kargs)
|
||||
super().__init__(*args, **kargs)
|
||||
|
||||
self.designated = designated
|
||||
|
||||
|
|
|
|||
67
nemubot/message/printer/IRCLib.py
Normal file
67
nemubot/message/printer/IRCLib.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2026 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 nemubot.message.visitor import AbstractVisitor
|
||||
|
||||
|
||||
class IRCLib(AbstractVisitor):
|
||||
|
||||
"""Visitor that sends bot responses via an irc.client.ServerConnection.
|
||||
|
||||
Unlike the socket-based IRC printer (which builds a raw PRIVMSG string),
|
||||
this calls connection.privmsg() directly so the library handles encoding,
|
||||
line-length capping, and any internal locking.
|
||||
"""
|
||||
|
||||
def __init__(self, connection):
|
||||
self._conn = connection
|
||||
|
||||
def _send(self, target, text):
|
||||
try:
|
||||
self._conn.privmsg(target, text)
|
||||
except Exception:
|
||||
pass # drop silently during reconnection
|
||||
|
||||
# Visitor methods
|
||||
|
||||
def visit_Text(self, msg):
|
||||
if isinstance(msg.message, str):
|
||||
for target in msg.to:
|
||||
self._send(target, msg.message)
|
||||
else:
|
||||
msg.message.accept(self)
|
||||
|
||||
def visit_DirectAsk(self, msg):
|
||||
text = msg.message if isinstance(msg.message, str) else str(msg.message)
|
||||
# Mirrors socket.py logic:
|
||||
# rooms that are NOT the designated nick get a "nick: " prefix
|
||||
others = [to for to in msg.to if to != msg.designated]
|
||||
if len(others) == 0 or len(others) != len(msg.to):
|
||||
for target in msg.to:
|
||||
self._send(target, text)
|
||||
if others:
|
||||
for target in others:
|
||||
self._send(target, "%s: %s" % (msg.designated, text))
|
||||
|
||||
def visit_Command(self, msg):
|
||||
parts = ["!" + msg.cmd] + list(msg.args)
|
||||
for target in msg.to:
|
||||
self._send(target, " ".join(parts))
|
||||
|
||||
def visit_OwnerCommand(self, msg):
|
||||
parts = ["`" + msg.cmd] + list(msg.args)
|
||||
for target in msg.to:
|
||||
self._send(target, " ".join(parts))
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue