Compare commits
57 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26f301d6b4 | |||
| ce4140ade8 | |||
| 2a7502e8e8 | |||
| 30c81c1c4b | |||
| 69dcd53937 | |||
| 2d9a533dc4 | |||
| fcff53d964 | |||
| c6b5aab917 | |||
| f26d95963e | |||
| 350e0f5f59 | |||
| 5aef661601 | |||
| d590282db8 | |||
| e70a7f4fe0 | |||
| a29325cb19 | |||
| 0cf1d37250 | |||
| 55bb6a090c | |||
| 27197b381d | |||
| 496f7d6399 | |||
| 4819e17a4e | |||
| 9c2acb9840 | |||
| 9b5a400ce9 | |||
| e3ebd7d05c | |||
| e947eccc48 | |||
| b2aa0cc5aa | |||
| 2df449fd96 | |||
| 9257abf9af | |||
| e04ea98f26 | |||
| 3dcd2e653d | |||
| db3d0043da | |||
| d59328c273 | |||
| fa79a730ae | |||
| c8941201d2 | |||
| d66d6c8ded | |||
| 2f2e989da6 | |||
| 4d65524aad | |||
| 3dbf8ed6ea | |||
| 8e0d746e4e | |||
| 9f8fa9f31f | |||
| 53bedd338a | |||
| c3b1c7534c | |||
| 1a5aca4844 | |||
| f60ab46274 | |||
| 8982965ed9 | |||
| d4302780da | |||
| 1f5cfb2ead | |||
| 838b76081d | |||
| b7e12037de | |||
| 302086d75b | |||
| ad23fadab1 | |||
| 1d554e0b0f | |||
| a624fca347 | |||
| 12403a3690 | |||
| 5f58f71d2f | |||
| 109b7440e0 | |||
| b1ad4bcf23 | |||
| 465bfefdab | |||
| 91230ac101 |
66 changed files with 1490 additions and 2205 deletions
26
.drone.yml
26
.drone.yml
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default-arm64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.11-alpine
|
||||
commands:
|
||||
- pip install --no-cache-dir -r requirements.txt
|
||||
- pip install .
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: nemunaire/nemubot
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
language: python
|
||||
python:
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
- nightly
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
|
|
|
|||
21
Dockerfile
21
Dockerfile
|
|
@ -1,21 +0,0 @@
|
|||
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" ]
|
||||
|
|
@ -7,10 +7,12 @@ An extremely modulable IRC bot, built around XML configuration files!
|
|||
Requirements
|
||||
------------
|
||||
|
||||
*nemubot* requires at least Python 3.3 to work.
|
||||
*nemubot* requires at least Python 3.4 to work, as it uses `asyncio`.
|
||||
|
||||
Connecting to SSL server requires [this patch](http://bugs.python.org/issue27629).
|
||||
|
||||
Some modules (like `cve`, `nextstop` or `laposte`) require the
|
||||
[BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/),
|
||||
[BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/),
|
||||
but the core and framework has no dependency.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@ def treat_alias(msg):
|
|||
|
||||
# Avoid infinite recursion
|
||||
if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd:
|
||||
return rpl_msg
|
||||
# Also return origin message, if it can be treated as well
|
||||
return [msg, rpl_msg]
|
||||
|
||||
return msg
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nemubot.event import ModuleEvent
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.countdown import countdown_format
|
||||
|
||||
|
|
@ -38,10 +37,8 @@ def load(context):
|
|||
chan = sayon["channel"]
|
||||
context.send_response(srv, Response(txt, chan))
|
||||
|
||||
d = datetime(yrn, 1, 1, 0, 0, 0, 0,
|
||||
timezone.utc) - datetime.now(timezone.utc)
|
||||
context.add_event(ModuleEvent(interval=0, offset=d.total_seconds(),
|
||||
call=bonneannee))
|
||||
context.call_at(datetime(yrn, 1, 1, 0, 0, 0, 0, timezone.utc),
|
||||
bonneannee)
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ for k, v in s:
|
|||
# MODULE CORE #########################################################
|
||||
|
||||
def get_conjug(verb, stringTens):
|
||||
url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
|
||||
url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
|
||||
quote(verb.encode("ISO-8859-1")))
|
||||
page = web.getURLContent(url)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ def get_info_yt(msg):
|
|||
|
||||
for line in soup.body.find_all('tr'):
|
||||
n = line.find_all('td')
|
||||
if len(n) == 7:
|
||||
res.append_message("\x02%s:\x0F from %s type %s at %s. Weight: %s. %s%s" %
|
||||
tuple([striphtml(x.text).strip() for x in n]))
|
||||
|
||||
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
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ VULN_DATAS = {
|
|||
"description": "vuln-description",
|
||||
"published": "vuln-published-on",
|
||||
"last_modified": "vuln-last-modified-on",
|
||||
"source": "vuln-source",
|
||||
|
||||
"base_score": "vuln-cvssv3-base-score-link",
|
||||
"severity": "vuln-cvssv3-base-score-severity",
|
||||
|
|
@ -91,9 +92,9 @@ def get_cve_desc(msg):
|
|||
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)
|
||||
res.append_message("{alert}From \x02{source}\x0F, 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)
|
||||
res.append_message("{alert}Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), from \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id)
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -78,12 +78,12 @@ def load(context):
|
|||
})
|
||||
|
||||
if context.data is None:
|
||||
context.set_default(DictNode())
|
||||
context.data = 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))
|
||||
context.data[kevt]._evt = context.call_at(context.data[kevt].end, partial(fini, kevt, context.data[kevt]))
|
||||
|
||||
|
||||
def fini(name, evt):
|
||||
|
|
@ -166,9 +166,6 @@ def start_countdown(msg):
|
|||
else:
|
||||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ def cmd_grep(msg):
|
|||
|
||||
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] + ").*?",
|
||||
l = [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?",
|
||||
" ".join(msg.args[1:]),
|
||||
msg,
|
||||
icase="nocase" in msg.kwargs,
|
||||
|
|
|
|||
|
|
@ -24,17 +24,20 @@ def get_movie_by_id(imdbid):
|
|||
|
||||
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(),
|
||||
"Title": soup.body.find(attrs={"itemprop": "name"}).next_element.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("div")[3].find_all("a")[:-1]]),
|
||||
"Duration": soup.body.find_all(attrs={"itemprop": "duration"})[-1].text.strip(),
|
||||
"imdbRating": soup.body.find(attrs={"itemprop": "ratingValue"}).text.strip(),
|
||||
"imdbVotes": soup.body.find(attrs={"itemprop": "ratingCount"}).text.strip(),
|
||||
"Plot": re.sub(r"\s+", " ", soup.body.find(id="titleStoryLine").find(attrs={"itemprop": "description"}).text).strip(),
|
||||
|
||||
"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"})]),
|
||||
"Type": "TV Series" if soup.find(attrs={"class": "np_episode_guide"}) else "Movie",
|
||||
"Country": ", ".join([c.find("a").text.strip() for c in soup.body.find(id="titleDetails").find_all(attrs={"class": "txt-block"}) if c.text.find("Country") != -1]),
|
||||
"Released": soup.body.find(attrs={"itemprop": "datePublished"}).attrs["content"] if "content" in soup.body.find(attrs={"itemprop": "datePublished"}).attrs else "N\A",
|
||||
"Genre": ", ".join([g.text.strip() for g in soup.body.find_all(attrs={"itemprop": "genre"})[:-1]]),
|
||||
"Director": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "director"})]),
|
||||
"Writer": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "creator"})]),
|
||||
"Actors": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "actors"})]),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -49,9 +52,7 @@ def find_movies(title, year=None):
|
|||
# Make the request
|
||||
data = web.getJSON(url, remove_callback=True)
|
||||
|
||||
if "d" not in data:
|
||||
return None
|
||||
elif year is None:
|
||||
if year is None:
|
||||
return data["d"]
|
||||
else:
|
||||
return [d for d in data["d"] if "y" in d and str(d["y"]) == year]
|
||||
|
|
@ -91,9 +92,9 @@ def cmd_imdb(msg):
|
|||
|
||||
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; \x02directed by:\x0F %s; \x02written by:\x0F %s; \x02main actors:\x0F %s"
|
||||
% (data['Type'], data['Country'], data['Released'], data['Director'], data['Writer'], data['Actors']))
|
||||
return res
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from nemubot.module.more import Response
|
|||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
URL_API = "https://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s"
|
||||
URL_API = "http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s"
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
|
@ -23,7 +23,7 @@ def load(context):
|
|||
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 https://developer.mapquest.com/")
|
||||
"/>\nRegister at http://developer.mapquest.com/")
|
||||
global URL_API
|
||||
URL_API = URL_API % context.config["apikey"].replace("%", "%%")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ def isup(url):
|
|||
|
||||
o = urllib.parse.urlparse(getNormalizedURL(url), "http")
|
||||
if o.netloc != "":
|
||||
isup = getJSON("https://isitup.org/%s.json" % o.netloc)
|
||||
isup = getJSON("http://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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def validator(url):
|
|||
raise IMException("Indicate a valid URL!")
|
||||
|
||||
try:
|
||||
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__})
|
||||
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__})
|
||||
raw = urllib.request.urlopen(req, timeout=10)
|
||||
except urllib.error.HTTPError as e:
|
||||
raise IMException("HTTP error occurs: %s %s" % (e.code, e.reason))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"""Alert on changes on websites"""
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from random import randint
|
||||
import urllib.parse
|
||||
|
|
@ -210,14 +209,15 @@ def start_watching(site, offset=0):
|
|||
offset -- offset time to delay the launch of the first check
|
||||
"""
|
||||
|
||||
#o = urlparse(getNormalizedURL(site["url"]), "http")
|
||||
#print("Add %s event for site: %s" % (site["type"], o.netloc))
|
||||
o = urlparse(getNormalizedURL(site["url"]), "http")
|
||||
#print_debug("Add %s event for site: %s" % (site["type"], o.netloc))
|
||||
|
||||
try:
|
||||
evt = ModuleEvent(func=partial(fwatch, url=site["url"]),
|
||||
cmp=site["lastcontent"],
|
||||
offset=offset, interval=site.getInt("time"),
|
||||
call=partial(alert_change, site=site))
|
||||
evt = ModuleEvent(func=fwatch,
|
||||
cmp_data=site["lastcontent"],
|
||||
func_data=site["url"], offset=offset,
|
||||
interval=site.getInt("time"),
|
||||
call=alert_change, call_data=site)
|
||||
site["_evt_id"] = add_event(evt)
|
||||
except IMException:
|
||||
logger.exception("Unable to watch %s", site["url"])
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from nemubot.tools.web import getJSON
|
|||
from nemubot.module.more import Response
|
||||
|
||||
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"
|
||||
URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s"
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
|
|
@ -22,7 +22,7 @@ def load(CONF, add_hook):
|
|||
"the !netwhois feature. Add it to the module "
|
||||
"configuration file:\n<whoisxmlapi username=\"XX\" "
|
||||
"password=\"XXX\" />\nRegister at "
|
||||
"https://www.whoisxmlapi.com/newaccount.php")
|
||||
"http://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"]))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from nemubot.hooks import hook
|
|||
from nemubot.tools import web
|
||||
|
||||
from nemubot.module.more import Response
|
||||
from nemubot.module.urlreducer import reduce_inline
|
||||
from nemubot.tools.feed import Feed, AtomEntry
|
||||
|
||||
|
||||
|
|
@ -51,11 +50,10 @@ def cmd_news(msg):
|
|||
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, line_treat=reduce_inline)
|
||||
res = Response(channel=msg.channel, nomore="No more news from %s" % url)
|
||||
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
|
||||
|
|
|
|||
229
modules/nntp.py
229
modules/nntp.py
|
|
@ -1,229 +0,0 @@
|
|||
"""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)
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
"""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)
|
||||
|
|
@ -40,7 +40,7 @@ def cmd_subreddit(msg):
|
|||
else:
|
||||
where = "r"
|
||||
|
||||
sbr = web.getJSON("https://www.reddit.com/%s/%s/about.json" %
|
||||
sbr = web.getJSON("http://www.reddit.com/%s/%s/about.json" %
|
||||
(where, sub.group(2)))
|
||||
|
||||
if sbr is None:
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -25,7 +25,7 @@ def cmd_tcode(msg):
|
|||
raise IMException("indicate a transaction code or "
|
||||
"a keyword to search!")
|
||||
|
||||
url = ("https://www.tcodesearch.com/tcodes/search?q=%s" %
|
||||
url = ("http://www.tcodesearch.com/tcodes/search?q=%s" %
|
||||
urllib.parse.quote(msg.args[0]))
|
||||
|
||||
page = web.getURLContent(url)
|
||||
|
|
|
|||
116
modules/smmry.py
116
modules/smmry.py
|
|
@ -1,116 +0,0 @@
|
|||
"""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
|
||||
|
|
@ -46,22 +46,29 @@ def send_sms(frm, api_usr, api_key, content):
|
|||
|
||||
return None
|
||||
|
||||
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:
|
||||
|
||||
@hook.command("sms")
|
||||
def cmd_sms(msg):
|
||||
if not len(msg.args):
|
||||
raise IMException("À qui veux-tu envoyer ce SMS ?")
|
||||
|
||||
# Check dests
|
||||
cur_epoch = time.mktime(time.localtime());
|
||||
for u in msg.args[0].split(","):
|
||||
if u not in context.data.index:
|
||||
raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u)
|
||||
elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42:
|
||||
raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u)
|
||||
return True
|
||||
|
||||
|
||||
def send_sms_to_list(msg, frm, dests, content, cur_epoch):
|
||||
# Go!
|
||||
fails = list()
|
||||
for u in dests:
|
||||
for u in msg.args[0].split(","):
|
||||
context.data.index[u]["lastuse"] = cur_epoch
|
||||
test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], content)
|
||||
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:]))
|
||||
if test is not None:
|
||||
fails.append( "%s: %s" % (u, test) )
|
||||
|
||||
|
|
@ -70,55 +77,6 @@ def send_sms_to_list(msg, frm, dests, content, cur_epoch):
|
|||
else:
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import re
|
|||
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.tools.web import getURLContent, getURLHeaders, getJSON
|
||||
from nemubot.tools.web import getURLContent, getJSON
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ from nemubot.module.more import Response
|
|||
|
||||
def get_tnt_info(track_id):
|
||||
values = []
|
||||
data = getURLContent('https://www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id)
|
||||
data = getURLContent('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:
|
||||
|
|
@ -31,22 +31,21 @@ def get_tnt_info(track_id):
|
|||
|
||||
|
||||
def get_colissimo_info(colissimo_id):
|
||||
colissimo_data = getURLContent("https://www.laposte.fr/particulier/outils/suivre-vos-envois?code=%s" % colissimo_id)
|
||||
colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id)
|
||||
soup = BeautifulSoup(colissimo_data)
|
||||
|
||||
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())
|
||||
dataArray = soup.find(class_='dataArray')
|
||||
if dataArray and dataArray.tbody and dataArray.tbody.tr:
|
||||
date = dataArray.tbody.tr.find(headers="Date").get_text()
|
||||
libelle = re.sub(r'[\n\t\r]', '',
|
||||
dataArray.tbody.tr.find(headers="Libelle").get_text())
|
||||
site = dataArray.tbody.tr.find(headers="site").get_text().strip()
|
||||
return (date, libelle, site.strip())
|
||||
|
||||
|
||||
def get_chronopost_info(track_id):
|
||||
data = urllib.parse.urlencode({'listeNumeros': track_id})
|
||||
track_baseurl = "https://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR"
|
||||
track_baseurl = "http://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR"
|
||||
track_data = getURLContent(track_baseurl, data.encode('utf-8'))
|
||||
soup = BeautifulSoup(track_data)
|
||||
|
||||
|
|
@ -75,29 +74,33 @@ def get_colisprive_info(track_id):
|
|||
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):
|
||||
status, laposte_headers = getURLHeaders("https://www.laposte.fr/outils/suivre-vos-envois?" + urllib.parse.urlencode({'code': laposte_id}))
|
||||
data = urllib.parse.urlencode({'id': laposte_id})
|
||||
laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index"
|
||||
|
||||
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]
|
||||
laposte_data = getURLContent(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_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_type = 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_date = field.get_text()
|
||||
|
||||
field = field.find_next('td')
|
||||
poste_location = field.get_text()
|
||||
|
||||
field = field.find_next('td')
|
||||
poste_status = field.get_text()
|
||||
|
||||
return (poste_type.lower(), poste_id.strip(), poste_status.lower(),
|
||||
poste_location, poste_date)
|
||||
|
||||
|
||||
def get_postnl_info(postnl_id):
|
||||
|
|
@ -128,7 +131,7 @@ def get_usps_info(usps_id):
|
|||
|
||||
usps_data = getURLContent(usps_parcelurl)
|
||||
soup = BeautifulSoup(usps_data)
|
||||
if (soup.find(id="trackingHistory_1")
|
||||
if (soup.find(class_="tracking_history")
|
||||
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()
|
||||
|
|
@ -172,8 +175,7 @@ def get_fedex_info(fedex_id, lang="en_US"):
|
|||
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"]["errorList"][0]["code"] and
|
||||
not fedex_data["TrackPackagesResponse"]["packageList"][0]["errorList"][0]["code"]
|
||||
):
|
||||
return fedex_data["TrackPackagesResponse"]["packageList"][0]
|
||||
|
|
@ -206,10 +208,11 @@ def handle_tnt(tracknum):
|
|||
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))
|
||||
poste_type, poste_id, poste_status, poste_location, poste_date = info
|
||||
return ("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))
|
||||
|
||||
|
||||
def handle_postnl(tracknum):
|
||||
|
|
@ -225,14 +228,7 @@ 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))
|
||||
return ("USPS \x02{tracknum}\x0F is {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_colissimo(tracknum):
|
||||
|
|
@ -285,7 +281,6 @@ TRACKING_HANDLERS = {
|
|||
'fedex': handle_fedex,
|
||||
'dhl': handle_dhl,
|
||||
'usps': handle_usps,
|
||||
'ups': handle_ups,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ def load(context):
|
|||
# MODULE CORE #########################################################
|
||||
|
||||
def get_french_synos(word):
|
||||
url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word)
|
||||
url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word)
|
||||
page = web.getURLContent(url)
|
||||
|
||||
best = list(); synos = list(); anton = list()
|
||||
|
|
@ -53,7 +53,7 @@ def get_french_synos(word):
|
|||
|
||||
|
||||
def get_english_synos(key, word):
|
||||
cnt = web.getJSON("https://words.bighugelabs.com/api/2/%s/%s/json" %
|
||||
cnt = web.getJSON("http://words.bighugelabs.com/api/2/%s/%s/json" %
|
||||
(quote(key), quote(word.encode("ISO-8859-1"))))
|
||||
|
||||
best = list(); synos = list(); anton = list()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from nemubot.module.more import Response
|
|||
|
||||
def search(terms):
|
||||
return web.getJSON(
|
||||
"https://api.urbandictionary.com/v0/define?term=%s"
|
||||
"http://api.urbandictionary.com/v0/define?term=%s"
|
||||
% quote(' '.join(terms)))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def default_reducer(url, data):
|
|||
|
||||
|
||||
def ycc_reducer(url, data):
|
||||
return "https://ycc.fr/%s" % default_reducer(url, data)
|
||||
return "http://ycc.fr/%s" % default_reducer(url, data)
|
||||
|
||||
def lstu_reducer(url, data):
|
||||
json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data),
|
||||
|
|
@ -36,8 +36,8 @@ def lstu_reducer(url, data):
|
|||
# MODULE VARIABLES ####################################################
|
||||
|
||||
PROVIDERS = {
|
||||
"tinyurl": (default_reducer, "https://tinyurl.com/api-create.php?url="),
|
||||
"ycc": (ycc_reducer, "https://ycc.fr/redirection/create/"),
|
||||
"tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="),
|
||||
"ycc": (ycc_reducer, "http://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"),
|
||||
|
|
@ -60,20 +60,12 @@ def load(context):
|
|||
|
||||
# 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):
|
||||
def reduce(url, provider=DEFAULT_PROVIDER):
|
||||
"""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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from nemubot.module import mapquest
|
|||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
URL_API = "https://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s"
|
||||
URL_API = "http://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s"
|
||||
|
||||
SPEED_TYPES = {
|
||||
0: 'Ground speed',
|
||||
|
|
|
|||
|
|
@ -19,63 +19,25 @@ from nemubot.module.more import Response
|
|||
|
||||
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 "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 https://developer.forecast.io/")
|
||||
"Register at http://developer.forecast.io/")
|
||||
context.data.setIndex("name", "city")
|
||||
global URL_DSAPI
|
||||
URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"]
|
||||
|
||||
|
||||
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 format_wth(wth):
|
||||
return ("{temperature} °C {summary}; precipitation ({precipProbability:.0%} chance) intensity: {precipIntensity} mm/h; relative humidity: {humidity:.0%}; wind speed: {windSpeed} km/s {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} hPa; ozone: {ozone} DU"
|
||||
.format(**wth)
|
||||
)
|
||||
|
||||
|
||||
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_forecast_daily(wth):
|
||||
return ("{summary}; between {temperatureMin}-{temperatureMax} °C; precipitation ({precipProbability:.0%} chance) intensity: maximum {precipIntensity} mm/h; relative humidity: {humidity:.0%}; wind speed: {windSpeed} km/h {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} hPa; ozone: {ozone} DU".format(**wth))
|
||||
|
||||
|
||||
def format_timestamp(timestamp, tzname, tzoffset, format="%c"):
|
||||
|
|
@ -126,7 +88,7 @@ def treat_coord(msg):
|
|||
raise IMException("indique-moi un nom de ville ou des coordonnées.")
|
||||
|
||||
|
||||
def get_json_weather(coords, lang="en", units="ca"):
|
||||
def get_json_weather(coords, lang="en", units="auto"):
|
||||
wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units))
|
||||
|
||||
# First read flags
|
||||
|
|
@ -152,13 +114,13 @@ def cmd_coordinates(msg):
|
|||
@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",
|
||||
"units=UNITS": "return weather conditions in the requested units; default: auto",
|
||||
})
|
||||
def cmd_alert(msg):
|
||||
loc, coords, specific = treat_coord(msg)
|
||||
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")
|
||||
units=msg.kwargs["units"] if "units" in msg.kwargs else "auto")
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)")
|
||||
|
||||
|
|
@ -179,13 +141,13 @@ def cmd_alert(msg):
|
|||
},
|
||||
keywords={
|
||||
"lang=LANG": "change the output language of weather sumarry; default: en",
|
||||
"units=UNITS": "return weather conditions in the requested units; default: ca",
|
||||
"units=UNITS": "return weather conditions in the requested units; default: auto",
|
||||
})
|
||||
def cmd_weather(msg):
|
||||
loc, coords, specific = treat_coord(msg)
|
||||
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")
|
||||
units=msg.kwargs["units"] if "units" in msg.kwargs else "auto")
|
||||
|
||||
res = Response(channel=msg.channel, nomore="No more weather information")
|
||||
|
||||
|
|
@ -207,17 +169,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, wth["flags"])))
|
||||
res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour)))
|
||||
|
||||
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, wth["flags"])))
|
||||
res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day)))
|
||||
|
||||
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"], wth["flags"]))
|
||||
res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"]))
|
||||
|
||||
nextres = "\x03\x02Today:\x03\x02 %s " % wth["daily"]["data"][0]["summary"]
|
||||
if "minutely" in wth:
|
||||
|
|
@ -227,11 +189,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, wth["flags"])))
|
||||
format_wth(hour)))
|
||||
|
||||
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, wth["flags"])))
|
||||
format_forecast_daily(day)))
|
||||
|
||||
return res
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ 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
|
||||
# You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/' > users.json
|
||||
APIEXTRACT_FILE = None
|
||||
|
||||
def load(context):
|
||||
|
|
@ -39,13 +39,17 @@ def load(context):
|
|||
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(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=None, login=None, uidNumber=None, firstname=None, lastname=None, promo=None, **kwargs):
|
||||
def __init__(self, line=None, login=None, uidNumber=None, cn=None, promo=None, **kwargs):
|
||||
if line is not None:
|
||||
s = line.split(":")
|
||||
self.login = s[0]
|
||||
|
|
@ -57,23 +61,19 @@ class Login:
|
|||
self.login = login
|
||||
self.uid = uidNumber
|
||||
self.promo = promo
|
||||
self.cn = firstname + " " + lastname
|
||||
try:
|
||||
self.gid = "epita" + str(int(promo))
|
||||
except:
|
||||
self.gid = promo
|
||||
self.cn = cn
|
||||
self.gid = "epita" + promo
|
||||
|
||||
def get_promo(self):
|
||||
if hasattr(self, "promo"):
|
||||
return self.promo
|
||||
if hasattr(self, "home"):
|
||||
try:
|
||||
return self.home.split("/")[2].replace("_", " ")
|
||||
except:
|
||||
return self.gid
|
||||
return self.home.split("/")[2].replace("_", " ")
|
||||
|
||||
def get_photo(self):
|
||||
for url in [ "https://photos.cri.epita.fr/%s", "https://intra-bocal.epitech.eu/trombi/%s.jpg" ]:
|
||||
if self.login in context.data.getNode("pics").index:
|
||||
return context.data.getNode("pics").index[self.login]["url"]
|
||||
for url in [ "https://photos.cri.epita.net/%s", "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 ]:
|
||||
url = url % self.login
|
||||
try:
|
||||
_, status, _, _ = headers(url)
|
||||
|
|
@ -91,7 +91,7 @@ def login_lookup(login, search=False):
|
|||
if APIEXTRACT_FILE:
|
||||
with open(APIEXTRACT_FILE, encoding="utf-8") as f:
|
||||
api = json.load(f)
|
||||
for l in api["results"]:
|
||||
for l in api:
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from nemubot.module.more import Response
|
|||
|
||||
# LOADING #############################################################
|
||||
|
||||
URL_API = "https://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s"
|
||||
URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s"
|
||||
|
||||
def load(context):
|
||||
global URL_API
|
||||
|
|
@ -24,7 +24,7 @@ def load(context):
|
|||
"this module. Add it to the module configuration: "
|
||||
"\n<module name=\"wolframalpha\" "
|
||||
"apikey=\"XXXXXX-XXXXXXXXXX\" />\n"
|
||||
"Register at https://products.wolframalpha.com/api/")
|
||||
"Register at http://products.wolframalpha.com/api/")
|
||||
URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""The 2014,2018 football worldcup module"""
|
||||
"""The 2014 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.event import ModuleEvent
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
|
|
@ -22,7 +20,8 @@ from nemubot.module.more import Response
|
|||
API_URL="http://worldcup.sfg.io/%s"
|
||||
|
||||
def load(context):
|
||||
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))
|
||||
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))
|
||||
|
||||
|
||||
def help_full ():
|
||||
|
|
@ -66,10 +65,10 @@ def cmd_watch(msg):
|
|||
context.save()
|
||||
raise IMException("This channel will not anymore receives world cup events.")
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
for match in matches:
|
||||
if is_valid(match):
|
||||
|
|
@ -121,19 +120,20 @@ def detail_event(evt):
|
|||
return evt + " par"
|
||||
|
||||
def txt_event(e):
|
||||
return "%s minute : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"])
|
||||
return "%se minutes : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"])
|
||||
|
||||
def prettify(match):
|
||||
matchdate = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%SZ").replace(tzinfo=timezone.utc)
|
||||
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))
|
||||
if match["status"] == "future":
|
||||
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"])]
|
||||
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"])]
|
||||
else:
|
||||
msgs = list()
|
||||
msg = ""
|
||||
if match["status"] == "completed":
|
||||
msg += "Match (%s) du %s terminé : " % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M"))
|
||||
msg += "Match (%s) du %s terminé : " % (match["match_number"], matchdate.strftime("%A %d à %H:%M"))
|
||||
else:
|
||||
msg += "Match en cours (%s) depuis %d minutes : " % (match["fifa_id"], (datetime.now(tz=timezone.utc) - matchdate).total_seconds() / 60)
|
||||
msg += "Match en cours (%s) depuis %d minutes : " % (match["match_number"], (datetime.now(matchdate.tzinfo) - matchdate_local).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,7 +163,7 @@ def is_valid(match):
|
|||
def get_match(url, matchid):
|
||||
allm = get_matches(url)
|
||||
for m in allm:
|
||||
if int(m["fifa_id"]) == matchid:
|
||||
if int(m["match_number"]) == matchid:
|
||||
return [ m ]
|
||||
|
||||
def get_matches(url):
|
||||
|
|
@ -192,7 +192,7 @@ 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.args[0])
|
||||
url = int(msg.arg[0])
|
||||
else:
|
||||
raise IMException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier")
|
||||
|
||||
|
|
|
|||
|
|
@ -39,14 +39,10 @@ def requires_version(min=None, max=None):
|
|||
"but this is nemubot v%s." % (str(max), __version__))
|
||||
|
||||
|
||||
def attach(pidfile, socketfile):
|
||||
def attach(pid, 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)
|
||||
|
|
@ -110,13 +106,28 @@ def attach(pidfile, socketfile):
|
|||
return 0
|
||||
|
||||
|
||||
def daemonize(socketfile=None):
|
||||
def daemonize(socketfile=None, autoattach=True):
|
||||
"""Detach the running process to run as a daemon
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if socketfile is not None:
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
if autoattach:
|
||||
import time
|
||||
os.waitpid(pid, 0)
|
||||
time.sleep(1)
|
||||
sys.exit(attach(pid, socketfile))
|
||||
else:
|
||||
sys.exit(0)
|
||||
except OSError as err:
|
||||
sys.stderr.write("Unable to fork: %s\n" % err)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
|
|
|
|||
|
|
@ -71,26 +71,12 @@ 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.pidfile = os.path.abspath(os.path.expanduser(args.pidfile))
|
||||
args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile))
|
||||
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)]
|
||||
|
||||
# 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")
|
||||
|
|
@ -120,7 +106,7 @@ def main():
|
|||
pass
|
||||
else:
|
||||
from nemubot import attach
|
||||
sys.exit(attach(args.pidfile, args.socketfile))
|
||||
sys.exit(attach(pid, args.socketfile))
|
||||
|
||||
# Add modules dir paths
|
||||
modules_paths = list()
|
||||
|
|
@ -155,18 +141,12 @@ def main():
|
|||
|
||||
# Preset each server in this file
|
||||
for server in config.servers:
|
||||
srv = server.server(config)
|
||||
# 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
|
||||
if context.add_server(srv):
|
||||
logger.info("Server '%s' successfully added.", srv.name)
|
||||
else:
|
||||
logger.error("Can't add server '%s'.", srv.name)
|
||||
|
||||
# Load module and their configuration
|
||||
for mod in config.modules:
|
||||
|
|
@ -195,28 +175,16 @@ def main():
|
|||
# Daemonize
|
||||
if not args.debug:
|
||||
from nemubot import daemonize
|
||||
daemonize(args.socketfile)
|
||||
daemonize(args.socketfile, not args.no_attach)
|
||||
|
||||
# Signals handling
|
||||
def sigtermhandler(signum, frame):
|
||||
def sigtermhandler():
|
||||
"""On SIGTERM and SIGINT, quit nicely"""
|
||||
context.quit()
|
||||
signal.signal(signal.SIGINT, sigtermhandler)
|
||||
signal.signal(signal.SIGTERM, sigtermhandler)
|
||||
context.loop.add_signal_handler(signal.SIGINT, sigtermhandler)
|
||||
context.loop.add_signal_handler(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):
|
||||
def sigusr1handler():
|
||||
"""On SIGHUSR1, display stacktraces"""
|
||||
import threading, traceback
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
|
|
@ -228,24 +196,22 @@ def main():
|
|||
logger.debug("########### Thread %s:\n%s",
|
||||
thName,
|
||||
"".join(traceback.format_stack(stack)))
|
||||
signal.signal(signal.SIGUSR1, sigusr1handler)
|
||||
context.loop.add_signal_handler(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()
|
||||
context.start()
|
||||
context.loop.set_debug(args.verbose > 0)
|
||||
context.loop.run_forever()
|
||||
context.join()
|
||||
|
||||
# Wait for consumers
|
||||
logger.info("Waiting for other threads shuts down...")
|
||||
if args.debug:
|
||||
sigusr1handler(0, None)
|
||||
sigusr1handler()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
|
|
|||
245
nemubot/bot.py
245
nemubot/bot.py
|
|
@ -14,10 +14,12 @@
|
|||
# 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 asyncio
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
from multiprocessing import JoinableQueue
|
||||
import threading
|
||||
import traceback
|
||||
import select
|
||||
import sys
|
||||
import weakref
|
||||
|
|
@ -40,7 +42,7 @@ 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(), debug=False):
|
||||
data_store=datastore.Abstract(), debug=False, loop=None):
|
||||
"""Initialize the bot context
|
||||
|
||||
Keyword arguments:
|
||||
|
|
@ -57,7 +59,17 @@ class Bot(threading.Thread):
|
|||
sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
|
||||
|
||||
self.debug = debug
|
||||
self.stop = True
|
||||
self.stop = None
|
||||
|
||||
#
|
||||
self.loop = loop if loop is not None else asyncio.get_event_loop()
|
||||
|
||||
# Those events are used to ensure there is always one event in the next 24h, else overflow can occurs on loop timeout
|
||||
def event_sentinel(offset=43210):
|
||||
logger.debug("Defining new event sentinelle in %ss", 43210 + offset)
|
||||
self.loop.call_later(43210 + offset, event_sentinel)
|
||||
event_sentinel(0)
|
||||
event_sentinel(43210)
|
||||
|
||||
# External IP for accessing this bot
|
||||
import ipaddress
|
||||
|
|
@ -74,10 +86,6 @@ class Bot(threading.Thread):
|
|||
self.modules = dict()
|
||||
self.modules_configuration = dict()
|
||||
|
||||
# Events
|
||||
self.events = list()
|
||||
self.event_timer = None
|
||||
|
||||
# Own hooks
|
||||
from nemubot.treatment import MessageTreater
|
||||
self.treater = MessageTreater()
|
||||
|
|
@ -135,24 +143,19 @@ class Bot(threading.Thread):
|
|||
"Vous pouvez le consulter, le dupliquer, "
|
||||
"envoyer des rapports de bogues ou bien "
|
||||
"contribuer au projet sur GitHub : "
|
||||
"https://github.com/nemunaire/nemubot/")
|
||||
"http://github.com/nemunaire/nemubot/")
|
||||
res.append_message(title="Pour plus de détails sur un module, "
|
||||
"envoyez \"!help nomdumodule\". Voici la liste"
|
||||
" de tous les modules disponibles localement",
|
||||
message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im]().__doc__) for im in self.modules if self.modules[im]() is not None and self.modules[im]().__doc__])
|
||||
message=["\x03\x02%s\x03\x02 (%s)" % (im.replace("nemubot.module.", ""), 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.Command(_help_msg, "help"), "in", "Command")
|
||||
|
||||
import os
|
||||
from queue import 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
|
||||
# Messages to be treated
|
||||
self.cnsr_queue = Queue()
|
||||
self.cnsr_thrd = list()
|
||||
self.cnsr_thrd_size = -1
|
||||
|
||||
|
||||
def __del__(self):
|
||||
|
|
@ -169,13 +172,8 @@ class Bot(threading.Thread):
|
|||
|
||||
self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI)
|
||||
|
||||
|
||||
self.stop = False
|
||||
|
||||
# Relaunch events
|
||||
self._update_event_timer()
|
||||
|
||||
logger.info("Starting main loop")
|
||||
self.stop = False
|
||||
while not self.stop:
|
||||
for fd, flag in self._poll.poll():
|
||||
# Handle internal socket passing orders
|
||||
|
|
@ -223,10 +221,7 @@ class Bot(threading.Thread):
|
|||
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
|
||||
self._poll.unregister(int(args[1]))
|
||||
except:
|
||||
logger.exception("Unhandled excpetion during action:")
|
||||
|
||||
|
|
@ -239,15 +234,14 @@ class Bot(threading.Thread):
|
|||
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()
|
||||
# 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
|
||||
|
||||
c = Consumer(self)
|
||||
self.cnsr_thrd.append(c)
|
||||
c.start()
|
||||
sync_queue = None
|
||||
logger.info("Ending main loop")
|
||||
|
||||
|
|
@ -255,7 +249,26 @@ class Bot(threading.Thread):
|
|||
|
||||
# Events methods
|
||||
|
||||
def add_event(self, evt, eid=None, module_src=None):
|
||||
@asyncio.coroutine
|
||||
def _call_at(self, when, *args, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def _add_event():
|
||||
return self.loop.call_at(when, *args, **kwargs)
|
||||
future = yield from asyncio.run_coroutine_threadsafe(_add_event(), loop=self.loop)
|
||||
logger.debug("New event registered, scheduled in %ss", when - self.loop.time())
|
||||
return future.result()
|
||||
|
||||
|
||||
def call_at(self, when, *args, **kwargs):
|
||||
delay = (when - datetime.now(timezone.utc)).total_seconds()
|
||||
return self._call_at(self.loop.time() + delay, *args, **kwargs)
|
||||
|
||||
|
||||
def call_delay(self, delay, *args, **kwargs):
|
||||
return self._call_at(self.loop.time() + delay, *args, **kwargs)
|
||||
|
||||
|
||||
def add_event(self, evt):
|
||||
"""Register an event and return its identifiant for futur update
|
||||
|
||||
Return:
|
||||
|
|
@ -264,125 +277,26 @@ class Bot(threading.Thread):
|
|||
|
||||
Argument:
|
||||
evt -- The event object to add
|
||||
|
||||
Keyword arguments:
|
||||
eid -- The desired event ID (object or string UUID)
|
||||
module_src -- The module to which the event is attached to
|
||||
"""
|
||||
|
||||
import uuid
|
||||
if hasattr(evt, "handle") and evt.handle is not None:
|
||||
raise Exception("Try to launch an already launched event.")
|
||||
|
||||
# Generate the event id if no given
|
||||
if eid is None:
|
||||
eid = uuid.uuid1()
|
||||
def _end_event_timer(event):
|
||||
"""Function called at the end of the event timer"""
|
||||
|
||||
# Fill the id field of the event
|
||||
if type(eid) is uuid.UUID:
|
||||
evt.id = str(eid)
|
||||
else:
|
||||
# Ok, this is quiet useless...
|
||||
try:
|
||||
evt.id = str(uuid.UUID(eid))
|
||||
except ValueError:
|
||||
evt.id = eid
|
||||
|
||||
# TODO: mutex here plz
|
||||
|
||||
# Add the event in its place
|
||||
t = evt.current
|
||||
i = 0 # sentinel
|
||||
for i in range(0, len(self.events)):
|
||||
if self.events[i].current > t:
|
||||
break
|
||||
self.events.insert(i, evt)
|
||||
|
||||
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:
|
||||
# Our event has been executed and removed from queue
|
||||
return None
|
||||
|
||||
# Register the event in the source module
|
||||
if module_src is not None:
|
||||
module_src.__nemubot_context__.events.append((evt, evt.id))
|
||||
evt.module_src = module_src
|
||||
|
||||
logger.info("New event registered in %d position: %s", i, t)
|
||||
return evt.id
|
||||
|
||||
|
||||
def del_event(self, evt, module_src=None):
|
||||
"""Find and remove an event from list
|
||||
|
||||
Return:
|
||||
True if the event has been found and removed, False else
|
||||
|
||||
Argument:
|
||||
evt -- The ModuleEvent object to remove or just the event identifier
|
||||
|
||||
Keyword arguments:
|
||||
module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent)
|
||||
"""
|
||||
|
||||
logger.info("Removing event: %s from %s", evt, module_src)
|
||||
|
||||
from nemubot.event import ModuleEvent
|
||||
if type(evt) is ModuleEvent:
|
||||
id = evt.id
|
||||
module_src = evt.module_src
|
||||
else:
|
||||
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()
|
||||
return True
|
||||
|
||||
for evt in self.events:
|
||||
if evt.id == id:
|
||||
self.events.remove(evt)
|
||||
|
||||
if module_src is not None:
|
||||
module_src.__nemubot_context__.events.remove((evt, evt.id))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _update_event_timer(self):
|
||||
"""(Re)launch the timer to end with the closest event"""
|
||||
|
||||
# Reset the timer if this is the first item
|
||||
if self.event_timer is not None:
|
||||
self.event_timer.cancel()
|
||||
|
||||
if len(self.events):
|
||||
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:
|
||||
logger.debug("Update timer: no timer left")
|
||||
|
||||
|
||||
def _end_event_timer(self):
|
||||
"""Function called at the end of the event timer"""
|
||||
|
||||
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))
|
||||
logger.debug("Trigering event")
|
||||
event.handle = None
|
||||
self.cnsr_queue.put_nowait(EventConsumer(event))
|
||||
sync_act("launch_consumer")
|
||||
|
||||
self._update_event_timer()
|
||||
evt.start(self.loop)
|
||||
evt.handle = call_at(evt._next, _end_event_timer, evt)
|
||||
|
||||
logger.debug("New event registered in %ss", evt._next - self.loop.time())
|
||||
|
||||
return evt.handle
|
||||
|
||||
|
||||
|
||||
# Consumers methods
|
||||
|
|
@ -425,8 +339,23 @@ class Bot(threading.Thread):
|
|||
def add_module(self, module):
|
||||
"""Add a module to the context, if already exists, unload the
|
||||
old one before"""
|
||||
|
||||
import nemubot.hooks
|
||||
|
||||
self.loop.call_soon_threadsafe(self._add_module,
|
||||
module,
|
||||
nemubot.hooks.hook.last_registered)
|
||||
|
||||
nemubot.hooks.hook.last_registered = []
|
||||
|
||||
|
||||
def _add_module(self, module, registered_functions):
|
||||
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)
|
||||
|
|
@ -448,14 +377,12 @@ 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 isinstance(module.__dict__[attr], _ModuleContext):
|
||||
module.__dict__[attr] = module.__nemubot_context__
|
||||
|
||||
# Register decorated functions
|
||||
import nemubot.hooks
|
||||
for s, h in nemubot.hooks.hook.last_registered:
|
||||
for s, h in registered_functions:
|
||||
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"):
|
||||
|
|
@ -511,10 +438,6 @@ class Bot(threading.Thread):
|
|||
def quit(self):
|
||||
"""Save and unload modules and disconnect servers"""
|
||||
|
||||
if self.event_timer is not None:
|
||||
logger.info("Stop the event timer...")
|
||||
self.event_timer.cancel()
|
||||
|
||||
logger.info("Save and unload all modules...")
|
||||
for mod in [m for m in self.modules.keys()]:
|
||||
self.unload_module(mod)
|
||||
|
|
@ -524,11 +447,13 @@ class Bot(threading.Thread):
|
|||
srv.close()
|
||||
|
||||
logger.info("Stop consumers")
|
||||
with self.cnsr_lock:
|
||||
k = list(self.cnsr_thrd)
|
||||
k = self.cnsr_thrd
|
||||
for cnsr in k:
|
||||
cnsr.stop = True
|
||||
|
||||
logger.info("Closing event loop")
|
||||
self.loop.stop()
|
||||
|
||||
if self.stop is False or sync_queue is not None:
|
||||
self.stop = True
|
||||
sync_act("end")
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class Server:
|
|||
return True
|
||||
|
||||
|
||||
def server(self, parent, trynb=0):
|
||||
def server(self, parent):
|
||||
from nemubot.server import factory
|
||||
|
||||
for a in ["nick", "owner", "realname", "encoding"]:
|
||||
|
|
@ -42,4 +42,4 @@ class Server:
|
|||
|
||||
self.caps += parent.caps
|
||||
|
||||
return factory(self.uri, caps=self.caps, channels=self.channels, trynb=trynb, **self.args)
|
||||
return factory(self.uri, caps=self.caps, channels=self.channels, **self.args)
|
||||
|
|
|
|||
|
|
@ -88,13 +88,8 @@ class EventConsumer:
|
|||
logger.exception("Error during event end")
|
||||
|
||||
# Reappend the event in the queue if it has next iteration
|
||||
if self.evt.next is not None:
|
||||
context.add_event(self.evt, eid=self.evt.id)
|
||||
|
||||
# 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, self.evt.id))
|
||||
if self.evt.next():
|
||||
context.add_event(self.evt)
|
||||
|
||||
|
||||
|
||||
|
|
@ -105,25 +100,18 @@ class Consumer(threading.Thread):
|
|||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.stop = False
|
||||
super().__init__(name="Nemubot consumer", daemon=True)
|
||||
super().__init__(name="Nemubot consumer")
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while not self.stop:
|
||||
try:
|
||||
stm = self.context.cnsr_queue.get(True, 1)
|
||||
except queue.Empty:
|
||||
break
|
||||
stm = self.context.cnsr_queue.get(True, 1)
|
||||
stm.run(self.context)
|
||||
self.context.cnsr_queue.task_done()
|
||||
|
||||
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
|
||||
except queue.Empty:
|
||||
pass
|
||||
finally:
|
||||
with self.context.cnsr_lock:
|
||||
self.context.cnsr_thrd.remove(self)
|
||||
self.context.cnsr_thrd_size -= 2
|
||||
self.context.cnsr_thrd.remove(self)
|
||||
|
|
|
|||
|
|
@ -21,84 +21,56 @@ class ModuleEvent:
|
|||
|
||||
"""Representation of a event initiated by a bot module"""
|
||||
|
||||
def __init__(self, call=None, func=None, cmp=None, interval=60, offset=0, times=1):
|
||||
def __init__(self, call=None, cmp=None, interval=60, offset=0, times=1):
|
||||
|
||||
"""Initialize the event
|
||||
|
||||
Keyword arguments:
|
||||
call -- Function to call when the event is realized
|
||||
func -- Function called to check
|
||||
cmp -- Boolean function called to check changes or value to compare with
|
||||
cmp -- Boolean function called to check changes
|
||||
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)
|
||||
"""
|
||||
|
||||
# What have we to check?
|
||||
self.func = func
|
||||
|
||||
# How detect a change?
|
||||
self.cmp = cmp
|
||||
|
||||
# What should we call when?
|
||||
self.call = call
|
||||
|
||||
# Store times
|
||||
# Time to wait before the first check
|
||||
if isinstance(offset, timedelta):
|
||||
self.offset = offset # Time to wait before the first check
|
||||
self.offset = offset
|
||||
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
|
||||
self.offset = timedelta(seconds=offset)
|
||||
self.interval = timedelta(seconds=interval)
|
||||
self._next = None # Cache
|
||||
|
||||
# How many times do this event?
|
||||
self.times = times
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
"""Return the date of the near check"""
|
||||
if self.times != 0:
|
||||
if self._end is None:
|
||||
self._end = datetime.now(timezone.utc) + self.offset + self.interval
|
||||
return self._end
|
||||
return None
|
||||
|
||||
@property
|
||||
def start(self, loop):
|
||||
if self._next is None:
|
||||
self._next = loop.time() + self.offset.total_seconds() + self.interval.total_seconds()
|
||||
|
||||
|
||||
def schedule(self, end):
|
||||
self.interval = timedelta(seconds=0)
|
||||
self.offset = end - datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def next(self):
|
||||
"""Return the date of the next check"""
|
||||
if self.times != 0:
|
||||
if self._end is None:
|
||||
return self.current
|
||||
elif self._end < datetime.now(timezone.utc):
|
||||
self._end += self.interval
|
||||
return self._end
|
||||
return None
|
||||
self._next += self.interval.total_seconds()
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def time_left(self):
|
||||
"""Return the time left before/after the near check"""
|
||||
if self.current is not None:
|
||||
return self.current - datetime.now(timezone.utc)
|
||||
return timedelta.max
|
||||
|
||||
def check(self):
|
||||
"""Run a check and realized the event if this is time"""
|
||||
|
||||
# Get new data
|
||||
if self.func is not None:
|
||||
d_new = self.func()
|
||||
else:
|
||||
d_new = None
|
||||
|
||||
# then compare with current data
|
||||
if self.cmp is None or (callable(self.cmp) and self.cmp(d_new)) or (not callable(self.cmp) and d_new != self.cmp):
|
||||
if self.cmp():
|
||||
self.times -= 1
|
||||
|
||||
# Call attended function
|
||||
if self.func is not None:
|
||||
self.call(d_new)
|
||||
else:
|
||||
self.call()
|
||||
self.call()
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -121,18 +119,10 @@ class Abstract:
|
|||
try:
|
||||
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)
|
||||
|
||||
if isinstance(ret, list):
|
||||
for r in ret:
|
||||
yield ret
|
||||
elif ret is not None:
|
||||
yield ret
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class Dict(Abstract):
|
|||
|
||||
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 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:
|
||||
|
|
|
|||
25
nemubot/message/printer/IRC.py
Normal file
25
nemubot/message/printer/IRC.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 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.message import Text
|
||||
from nemubot.message.printer.socket import Socket as SocketPrinter
|
||||
|
||||
|
||||
class IRC(SocketPrinter):
|
||||
|
||||
def visit_Text(self, msg):
|
||||
self.pp += "PRIVMSG %s :" % ",".join(msg.to)
|
||||
super().visit_Text(msg)
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# 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))
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
# 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 Matrix(AbstractVisitor):
|
||||
|
||||
"""Visitor that sends bot responses as Matrix room messages.
|
||||
|
||||
Instead of accumulating text like the IRC printer does, each visit_*
|
||||
method calls send_func(room_id, text) directly for every destination room.
|
||||
"""
|
||||
|
||||
def __init__(self, send_func):
|
||||
"""
|
||||
Argument:
|
||||
send_func -- callable(room_id: str, text: str) that sends a plain-text
|
||||
message to the given Matrix room
|
||||
"""
|
||||
self._send = send_func
|
||||
|
||||
def visit_Text(self, msg):
|
||||
if isinstance(msg.message, str):
|
||||
for room in msg.to:
|
||||
self._send(room, msg.message)
|
||||
else:
|
||||
# Nested message object — let it visit itself
|
||||
msg.message.accept(self)
|
||||
|
||||
def visit_DirectAsk(self, msg):
|
||||
text = msg.message if isinstance(msg.message, str) else str(msg.message)
|
||||
# Rooms that are NOT the designated nick → prefix with "nick: "
|
||||
others = [to for to in msg.to if to != msg.designated]
|
||||
if len(others) == 0 or len(others) != len(msg.to):
|
||||
# At least one room IS the designated target → send plain
|
||||
for room in msg.to:
|
||||
self._send(room, text)
|
||||
if len(others):
|
||||
# Other rooms → prefix with nick
|
||||
for room in others:
|
||||
self._send(room, "%s: %s" % (msg.designated, text))
|
||||
|
||||
def visit_Command(self, msg):
|
||||
parts = ["!" + msg.cmd]
|
||||
if msg.args:
|
||||
parts.extend(msg.args)
|
||||
for room in msg.to:
|
||||
self._send(room, " ".join(parts))
|
||||
|
||||
def visit_OwnerCommand(self, msg):
|
||||
parts = ["`" + msg.cmd]
|
||||
if msg.args:
|
||||
parts.extend(msg.args)
|
||||
for room in msg.to:
|
||||
self._send(room, " ".join(parts))
|
||||
|
|
@ -181,16 +181,13 @@ class Response:
|
|||
return self.nomore
|
||||
|
||||
if self.line_treat is not None and self.elt == 0:
|
||||
try:
|
||||
if isinstance(self.messages[0], list):
|
||||
for x in self.messages[0]:
|
||||
print(x, self.line_treat(x))
|
||||
self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]]
|
||||
else:
|
||||
self.messages[0] = (self.line_treat(self.messages[0])
|
||||
.replace("\n", " ").strip())
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
if isinstance(self.messages[0], list):
|
||||
for x in self.messages[0]:
|
||||
print(x, self.line_treat(x))
|
||||
self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]]
|
||||
else:
|
||||
self.messages[0] = (self.line_treat(self.messages[0])
|
||||
.replace("\n", " ").strip())
|
||||
|
||||
msg = ""
|
||||
if self.title is not None:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,27 @@
|
|||
# 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 asyncio
|
||||
|
||||
|
||||
class _TinyEvent:
|
||||
|
||||
def __init__(self, handle):
|
||||
self.handle = handle
|
||||
|
||||
|
||||
class _FakeHandle:
|
||||
|
||||
def __init__(self, true_handle, callback):
|
||||
self.handle = true_handle
|
||||
self.callback = callback
|
||||
|
||||
def cancel(self):
|
||||
self.handle.cancel()
|
||||
if self.callback:
|
||||
return self.callback()
|
||||
|
||||
|
||||
class _ModuleContext:
|
||||
|
||||
def __init__(self, module=None, knodes=None):
|
||||
|
|
@ -24,8 +45,8 @@ class _ModuleContext:
|
|||
else:
|
||||
self.module_name = ""
|
||||
|
||||
self.hooks = list()
|
||||
self.events = list()
|
||||
self.hooks = list()
|
||||
self.debug = False
|
||||
|
||||
from nemubot.config.module import Module
|
||||
|
|
@ -37,13 +58,6 @@ class _ModuleContext:
|
|||
from nemubot.tools.xmlparser import module_state
|
||||
return module_state.ModuleState("nemubotstate")
|
||||
|
||||
def set_knodes(self, knodes):
|
||||
self._knodes = knodes
|
||||
|
||||
def set_default(self, default):
|
||||
# Access to data will trigger the load of data
|
||||
if self.data is None:
|
||||
self._data = default
|
||||
|
||||
def add_hook(self, hook, *triggers):
|
||||
from nemubot.hooks import Abstract as AbstractHook
|
||||
|
|
@ -55,19 +69,22 @@ class _ModuleContext:
|
|||
assert isinstance(hook, AbstractHook), hook
|
||||
self.hooks.remove((triggers, hook))
|
||||
|
||||
|
||||
def subtreat(self, msg):
|
||||
return None
|
||||
|
||||
def add_event(self, evt, eid=None):
|
||||
return self.events.append((evt, eid))
|
||||
|
||||
def set_knodes(self, knodes):
|
||||
self._knodes = knodes
|
||||
|
||||
|
||||
def add_event(self, evt):
|
||||
self.events.append(evt)
|
||||
return evt
|
||||
|
||||
def del_event(self, evt):
|
||||
for i in self.events:
|
||||
e, eid = i
|
||||
if e == evt:
|
||||
self.events.remove(i)
|
||||
return True
|
||||
return False
|
||||
return self.events.remove(evt)
|
||||
|
||||
|
||||
def send_response(self, server, res):
|
||||
self.module.logger.info("Send response: %s", res)
|
||||
|
|
@ -85,6 +102,15 @@ class _ModuleContext:
|
|||
self._data = self.load_data()
|
||||
return self._data
|
||||
|
||||
@data.setter
|
||||
def data(self, data):
|
||||
self._data = data
|
||||
return self._data
|
||||
|
||||
@data.deleter
|
||||
def data(self):
|
||||
self._data = None
|
||||
|
||||
|
||||
def unload(self):
|
||||
"""Perform actions for unloading the module"""
|
||||
|
|
@ -94,7 +120,7 @@ class _ModuleContext:
|
|||
self.del_hook(h, *s)
|
||||
|
||||
# Remove registered events
|
||||
for evt, eid in self.events:
|
||||
for evt in self.events:
|
||||
self.del_event(evt)
|
||||
|
||||
self.save()
|
||||
|
|
@ -123,6 +149,10 @@ class ModuleContext(_ModuleContext):
|
|||
def load_data(self):
|
||||
return self.context.datastore.load(self.module_name, self._knodes)
|
||||
|
||||
def save(self):
|
||||
self.context.datastore.save(self.module_name, self.data)
|
||||
|
||||
|
||||
def add_hook(self, hook, *triggers):
|
||||
from nemubot.hooks import Abstract as AbstractHook
|
||||
assert isinstance(hook, AbstractHook), hook
|
||||
|
|
@ -135,14 +165,41 @@ class ModuleContext(_ModuleContext):
|
|||
self.hooks.remove((triggers, hook))
|
||||
return self.context.treater.hm.del_hooks(*triggers, hook=hook)
|
||||
|
||||
|
||||
def subtreat(self, msg):
|
||||
yield from self.context.treater.treat_msg(msg)
|
||||
|
||||
def add_event(self, evt, eid=None):
|
||||
return self.context.add_event(evt, eid, module_src=self.module)
|
||||
|
||||
def _add_event(self, evt, call_add, *args, **kwargs):
|
||||
if evt in self.events:
|
||||
return None
|
||||
|
||||
def _cancel_event():
|
||||
self.module.logger.debug("Cancel event")
|
||||
evt.handle = None
|
||||
return super(ModuleContext, self).del_event(evt)
|
||||
|
||||
hd = call_add(*args, **kwargs)
|
||||
evt.handle = _FakeHandle(hd, _cancel_event)
|
||||
|
||||
return super().add_event(evt)
|
||||
|
||||
|
||||
def add_event(self, evt):
|
||||
return self._add_event(evt, self.context.add_event, evt)
|
||||
|
||||
def call_at(self, *args, **kwargs):
|
||||
evt = _TinyEvent(None)
|
||||
return self._add_event(evt, self.context.call_at, *args, **kwargs)
|
||||
|
||||
def call_later(self, *args, **kwargs):
|
||||
evt = _TinyEvent(None)
|
||||
return self._add_event(evt, self.context.call_later, *args, **kwargs)
|
||||
|
||||
def del_event(self, evt):
|
||||
return self.context.del_event(evt, module_src=self.module)
|
||||
# Call to super().del_event is done in the _FakeHandle.cancel
|
||||
return evt.handle.cancel()
|
||||
|
||||
|
||||
def send_response(self, server, res):
|
||||
if server in self.context.servers:
|
||||
|
|
|
|||
239
nemubot/server/DCC.py
Normal file
239
nemubot/server/DCC.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
# 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 imp
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import nemubot.message as message
|
||||
import nemubot.server as server
|
||||
|
||||
#Store all used ports
|
||||
PORTS = list()
|
||||
|
||||
class DCC(server.AbstractServer):
|
||||
def __init__(self, srv, dest, socket=None):
|
||||
super().__init__(name="Nemubot DCC server")
|
||||
|
||||
self.error = False # An error has occur, closing the connection?
|
||||
self.messages = list() # Message queued before connexion
|
||||
|
||||
# Informations about the sender
|
||||
self.sender = dest
|
||||
if self.sender is not None:
|
||||
self.nick = (self.sender.split('!'))[0]
|
||||
if self.nick != self.sender:
|
||||
self.realname = (self.sender.split('!'))[1]
|
||||
else:
|
||||
self.realname = self.nick
|
||||
|
||||
# Keep the server
|
||||
self.srv = srv
|
||||
self.treatement = self.treat_msg
|
||||
|
||||
# Found a port for the connection
|
||||
self.port = self.foundPort()
|
||||
|
||||
if self.port is None:
|
||||
self.logger.critical("No more available slot for DCC connection")
|
||||
self.setError("Il n'y a plus de place disponible sur le serveur"
|
||||
" pour initialiser une session DCC.")
|
||||
|
||||
def foundPort(self):
|
||||
"""Found a free port for the connection"""
|
||||
for p in range(65432, 65535):
|
||||
if p not in PORTS:
|
||||
PORTS.append(p)
|
||||
return p
|
||||
return None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Gives the server identifiant"""
|
||||
return self.srv.id + "/" + self.sender
|
||||
|
||||
def setError(self, msg):
|
||||
self.error = True
|
||||
self.srv.send_msg_usr(self.sender, msg)
|
||||
|
||||
def accept_user(self, host, port):
|
||||
"""Accept a DCC connection"""
|
||||
self.s = socket.socket()
|
||||
try:
|
||||
self.s.connect((host, port))
|
||||
self.logger.info("Accepted user from %s:%d for %s", host, port, self.sender)
|
||||
self.connected = True
|
||||
self.stop = False
|
||||
except:
|
||||
self.connected = False
|
||||
self.error = True
|
||||
return False
|
||||
self.start()
|
||||
return True
|
||||
|
||||
|
||||
def request_user(self, type="CHAT", filename="CHAT", size=""):
|
||||
"""Create a DCC connection"""
|
||||
#Open the port
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.bind(('', self.port))
|
||||
except:
|
||||
try:
|
||||
self.port = self.foundPort()
|
||||
s.bind(('', self.port))
|
||||
except:
|
||||
self.setError("Une erreur s'est produite durant la tentative"
|
||||
" d'ouverture d'une session DCC.")
|
||||
return False
|
||||
self.logger.info("Listening on %d for %s", self.port, self.sender)
|
||||
|
||||
#Send CTCP request for DCC
|
||||
self.srv.send_ctcp(self.sender,
|
||||
"DCC %s %s %d %d %s" % (type, filename, self.srv.ip,
|
||||
self.port, size),
|
||||
"PRIVMSG")
|
||||
|
||||
s.listen(1)
|
||||
#Waiting for the client
|
||||
(self.s, addr) = s.accept()
|
||||
self.logger.info("Connected by %d", addr)
|
||||
self.connected = True
|
||||
return True
|
||||
|
||||
def send_dcc_raw(self, line):
|
||||
self.s.sendall(line + b'\n')
|
||||
|
||||
def send_dcc(self, msg, to = None):
|
||||
"""If we talk to this user, send a message through this connection
|
||||
else, send the message to the server class"""
|
||||
if to is None or to == self.sender or to == self.nick:
|
||||
if self.error:
|
||||
self.srv.send_msg_final(self.nick, msg)
|
||||
elif not self.connected or self.s is None:
|
||||
try:
|
||||
self.start()
|
||||
except RuntimeError:
|
||||
pass
|
||||
self.messages.append(msg)
|
||||
else:
|
||||
for line in msg.split("\n"):
|
||||
self.send_dcc_raw(line.encode())
|
||||
else:
|
||||
self.srv.send_dcc(msg, to)
|
||||
|
||||
def send_file(self, filename):
|
||||
"""Send a file over DCC"""
|
||||
if os.path.isfile(filename):
|
||||
self.messages = filename
|
||||
try:
|
||||
self.start()
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
self.logger.error("File not found `%s'", filename)
|
||||
|
||||
def run(self):
|
||||
self.stopping.clear()
|
||||
|
||||
# Send file connection
|
||||
if not isinstance(self.messages, list):
|
||||
self.request_user("SEND",
|
||||
os.path.basename(self.messages),
|
||||
os.path.getsize(self.messages))
|
||||
if self.connected:
|
||||
with open(self.messages, 'rb') as f:
|
||||
d = f.read(268435456) #Packets size: 256Mo
|
||||
while d:
|
||||
self.s.sendall(d)
|
||||
self.s.recv(4) #The client send a confirmation after each packet
|
||||
d = f.read(268435456) #Packets size: 256Mo
|
||||
|
||||
# Messages connection
|
||||
else:
|
||||
if not self.connected:
|
||||
if not self.request_user():
|
||||
#TODO: do something here
|
||||
return False
|
||||
|
||||
#Start by sending all queued messages
|
||||
for mess in self.messages:
|
||||
self.send_dcc(mess)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
readbuffer = b''
|
||||
self.nicksize = len(self.srv.nick)
|
||||
self.Bnick = self.srv.nick.encode()
|
||||
while not self.stop:
|
||||
raw = self.s.recv(1024) #recieve server messages
|
||||
if not raw:
|
||||
break
|
||||
readbuffer = readbuffer + raw
|
||||
temp = readbuffer.split(b'\n')
|
||||
readbuffer = temp.pop()
|
||||
|
||||
for line in temp:
|
||||
self.treatement(line)
|
||||
|
||||
if self.connected:
|
||||
self.s.close()
|
||||
self.connected = False
|
||||
|
||||
#Remove from DCC connections server list
|
||||
if self.realname in self.srv.dcc_clients:
|
||||
del self.srv.dcc_clients[self.realname]
|
||||
|
||||
self.logger.info("Closing connection with %s", self.nick)
|
||||
self.stopping.set()
|
||||
if self.closing_event is not None:
|
||||
self.closing_event()
|
||||
#Rearm Thread
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def treat_msg(self, line):
|
||||
"""Treat a receive message, *can be overwritten*"""
|
||||
if line == b'NEMUBOT###':
|
||||
bot = self.srv.add_networkbot(self.srv, self.sender, self)
|
||||
self.treatement = bot.treat_msg
|
||||
self.send_dcc("NEMUBOT###")
|
||||
elif (line[:self.nicksize] == self.Bnick and
|
||||
line[self.nicksize+1:].strip()[:10] == b'my name is'):
|
||||
name = line[self.nicksize+1:].strip()[11:].decode('utf-8',
|
||||
'replace')
|
||||
if re.match("^[a-zA-Z0-9_-]+$", name):
|
||||
if name not in self.srv.dcc_clients:
|
||||
del self.srv.dcc_clients[self.sender]
|
||||
self.nick = name
|
||||
self.sender = self.nick + "!" + self.realname
|
||||
self.srv.dcc_clients[self.realname] = self
|
||||
self.send_dcc("Hi " + self.nick)
|
||||
else:
|
||||
self.send_dcc("This nickname is already in use"
|
||||
", please choose another one.")
|
||||
else:
|
||||
self.send_dcc("The name you entered contain"
|
||||
" invalid char.")
|
||||
else:
|
||||
self.srv.treat_msg(
|
||||
(":%s PRIVMSG %s :" % (
|
||||
self.sender,self.srv.nick)).encode() + line,
|
||||
True)
|
||||
283
nemubot/server/IRC.py
Normal file
283
nemubot/server/IRC.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
# 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/>.
|
||||
|
||||
from datetime import datetime
|
||||
import re
|
||||
import socket
|
||||
|
||||
from nemubot.channel import Channel
|
||||
from nemubot.message.printer.IRC import IRC as IRCPrinter
|
||||
from nemubot.server.message.IRC import IRC as IRCMessage
|
||||
from nemubot.server.socket import SocketServer, SecureSocketServer
|
||||
|
||||
|
||||
class _IRC:
|
||||
|
||||
"""Concrete implementation of a connexion to an IRC server"""
|
||||
|
||||
def __init__(self, host="localhost", port=6667, owner=None,
|
||||
nick="nemubot", username=None, password=None,
|
||||
realname="Nemubot", encoding="utf-8", caps=None,
|
||||
channels=list(), on_connect=None, **kwargs):
|
||||
"""Prepare a connection with an IRC server
|
||||
|
||||
Keyword arguments:
|
||||
host -- host to join
|
||||
port -- port on the host to reach
|
||||
ssl -- is this server using a TLS socket
|
||||
owner -- bot's owner
|
||||
nick -- bot's nick
|
||||
username -- the username as sent to server
|
||||
password -- if a password is required to connect to the server
|
||||
realname -- the bot's realname
|
||||
encoding -- the encoding used on the whole server
|
||||
caps -- client capabilities to register on the server
|
||||
channels -- list of channels to join on connection
|
||||
on_connect -- generator to call when connection is done
|
||||
"""
|
||||
|
||||
self.username = username if username is not None else nick
|
||||
self.password = password
|
||||
self.nick = nick
|
||||
self.owner = owner
|
||||
self.realname = realname
|
||||
|
||||
super().__init__(name=self.username + "@" + host + ":" + str(port),
|
||||
host=host, port=port, **kwargs)
|
||||
self.printer = IRCPrinter
|
||||
|
||||
self.encoding = encoding
|
||||
|
||||
# Keep a list of joined channels
|
||||
self.channels = dict()
|
||||
|
||||
# Server/client capabilities
|
||||
self.capabilities = caps
|
||||
|
||||
# Register CTCP capabilities
|
||||
self.ctcp_capabilities = dict()
|
||||
|
||||
def _ctcp_clientinfo(msg, cmds):
|
||||
"""Response to CLIENTINFO CTCP message"""
|
||||
return " ".join(self.ctcp_capabilities.keys())
|
||||
|
||||
def _ctcp_dcc(msg, cmds):
|
||||
"""Response to DCC CTCP message"""
|
||||
try:
|
||||
import ipaddress
|
||||
ip = ipaddress.ip_address(int(cmds[3]))
|
||||
port = int(cmds[4])
|
||||
conn = DCC(srv, msg.sender)
|
||||
except:
|
||||
return "ERRMSG invalid parameters provided as DCC CTCP request"
|
||||
|
||||
self.logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port)
|
||||
|
||||
if conn.accept_user(ip, port):
|
||||
srv.dcc_clients[conn.sender] = conn
|
||||
conn.send_dcc("Hello %s!" % conn.nick)
|
||||
else:
|
||||
self.logger.error("DCC: unable to connect to %s:%d", ip, port)
|
||||
return "ERRMSG unable to connect to %s:%d" % (ip, port)
|
||||
|
||||
import nemubot
|
||||
|
||||
self.ctcp_capabilities["ACTION"] = lambda msg, cmds: None
|
||||
self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo
|
||||
#self.ctcp_capabilities["DCC"] = _ctcp_dcc
|
||||
self.ctcp_capabilities["FINGER"] = lambda msg, cmds: "VERSION nemubot v%s" % nemubot.__version__
|
||||
self.ctcp_capabilities["NEMUBOT"] = lambda msg, cmds: "NEMUBOT %s" % nemubot.__version__
|
||||
self.ctcp_capabilities["PING"] = lambda msg, cmds: "PING %s" % " ".join(cmds[1:])
|
||||
self.ctcp_capabilities["SOURCE"] = lambda msg, cmds: "SOURCE https://github.com/nemunaire/nemubot"
|
||||
self.ctcp_capabilities["TIME"] = lambda msg, cmds: "TIME %s" % (datetime.now())
|
||||
self.ctcp_capabilities["USERINFO"] = lambda msg, cmds: "USERINFO %s" % self.realname
|
||||
self.ctcp_capabilities["VERSION"] = lambda msg, cmds: "VERSION nemubot v%s" % nemubot.__version__
|
||||
|
||||
# TODO: Temporary fix, waiting for hook based CTCP management
|
||||
self.ctcp_capabilities["TYPING"] = lambda msg, cmds: None
|
||||
|
||||
self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities))
|
||||
|
||||
|
||||
# Register hooks on some IRC CMD
|
||||
self.hookscmd = dict()
|
||||
|
||||
# Respond to PING
|
||||
def _on_ping(msg):
|
||||
self.write(b"PONG :" + msg.params[0])
|
||||
self.hookscmd["PING"] = _on_ping
|
||||
|
||||
# Respond to 001
|
||||
def _on_connect(msg):
|
||||
# First, send user defined command
|
||||
if on_connect is not None:
|
||||
if callable(on_connect):
|
||||
toc = on_connect()
|
||||
else:
|
||||
toc = on_connect
|
||||
if toc is not None:
|
||||
for oc in toc:
|
||||
self.write(oc)
|
||||
# Then, JOIN some channels
|
||||
for chn in channels:
|
||||
if chn.password:
|
||||
self.write("JOIN %s %s" % (chn.name, chn.password))
|
||||
else:
|
||||
self.write("JOIN %s" % chn.name)
|
||||
self.hookscmd["001"] = _on_connect
|
||||
|
||||
# Respond to ERROR
|
||||
def _on_error(msg):
|
||||
self.close()
|
||||
self.hookscmd["ERROR"] = _on_error
|
||||
|
||||
# Respond to CAP
|
||||
def _on_cap(msg):
|
||||
if len(msg.params) != 3 or msg.params[1] != b"LS":
|
||||
return
|
||||
server_caps = msg.params[2].decode().split(" ")
|
||||
for cap in self.capabilities:
|
||||
if cap not in server_caps:
|
||||
self.capabilities.remove(cap)
|
||||
if len(self.capabilities) > 0:
|
||||
self.write("CAP REQ :" + " ".join(self.capabilities))
|
||||
self.write("CAP END")
|
||||
self.hookscmd["CAP"] = _on_cap
|
||||
|
||||
# Respond to JOIN
|
||||
def _on_join(msg):
|
||||
if len(msg.params) == 0:
|
||||
return
|
||||
|
||||
for chname in msg.decode(msg.params[0]).split(","):
|
||||
# Register the channel
|
||||
chan = Channel(chname)
|
||||
self.channels[chname] = chan
|
||||
self.hookscmd["JOIN"] = _on_join
|
||||
# Respond to PART
|
||||
def _on_part(msg):
|
||||
if len(msg.params) != 1 and len(msg.params) != 2:
|
||||
return
|
||||
|
||||
for chname in msg.params[0].split(b","):
|
||||
if chname in self.channels:
|
||||
if msg.frm == self.nick:
|
||||
del self.channels[chname]
|
||||
elif msg.frm in self.channels[chname].people:
|
||||
del self.channels[chname].people[msg.frm]
|
||||
self.hookscmd["PART"] = _on_part
|
||||
# Respond to 331/RPL_NOTOPIC,332/RPL_TOPIC,TOPIC
|
||||
def _on_topic(msg):
|
||||
if len(msg.params) != 1 and len(msg.params) != 2:
|
||||
return
|
||||
if msg.params[0] in self.channels:
|
||||
if len(msg.params) == 1 or len(msg.params[1]) == 0:
|
||||
self.channels[msg.params[0]].topic = None
|
||||
else:
|
||||
self.channels[msg.params[0]].topic = msg.decode(msg.params[1])
|
||||
self.hookscmd["331"] = _on_topic
|
||||
self.hookscmd["332"] = _on_topic
|
||||
self.hookscmd["TOPIC"] = _on_topic
|
||||
# Respond to 353/RPL_NAMREPLY
|
||||
def _on_353(msg):
|
||||
if len(msg.params) == 3:
|
||||
msg.params.pop(0) # 353: like RFC 1459
|
||||
if len(msg.params) != 2:
|
||||
return
|
||||
if msg.params[0] in self.channels:
|
||||
for nk in msg.decode(msg.params[1]).split(" "):
|
||||
res = re.match("^(?P<level>[^a-zA-Z[\]\\`_^{|}])(?P<nickname>[a-zA-Z[\]\\`_^{|}][a-zA-Z0-9[\]\\`_^{|}-]*)$")
|
||||
self.channels[msg.params[0]].people[res.group("nickname")] = res.group("level")
|
||||
self.hookscmd["353"] = _on_353
|
||||
|
||||
# Respond to INVITE
|
||||
def _on_invite(msg):
|
||||
if len(msg.params) != 2:
|
||||
return
|
||||
self.write("JOIN " + msg.decode(msg.params[1]))
|
||||
self.hookscmd["INVITE"] = _on_invite
|
||||
|
||||
# Respond to ERR_NICKCOLLISION
|
||||
def _on_nickcollision(msg):
|
||||
self.nick += "_"
|
||||
self.write("NICK " + self.nick)
|
||||
self.hookscmd["433"] = _on_nickcollision
|
||||
self.hookscmd["436"] = _on_nickcollision
|
||||
|
||||
# Handle CTCP requests
|
||||
def _on_ctcp(msg):
|
||||
if len(msg.params) != 2 or not msg.is_ctcp:
|
||||
return
|
||||
cmds = msg.decode(msg.params[1][1:len(msg.params[1])-1]).split(' ')
|
||||
if cmds[0] in self.ctcp_capabilities:
|
||||
res = self.ctcp_capabilities[cmds[0]](msg, cmds)
|
||||
else:
|
||||
res = "ERRMSG Unknown or unimplemented CTCP request"
|
||||
if res is not None:
|
||||
self.write("NOTICE %s :\x01%s\x01" % (msg.frm, res))
|
||||
self.hookscmd["PRIVMSG"] = _on_ctcp
|
||||
|
||||
|
||||
# Open/close
|
||||
|
||||
def connect(self):
|
||||
super().connect()
|
||||
|
||||
if self.password is not None:
|
||||
self.write("PASS :" + self.password)
|
||||
if self.capabilities is not None:
|
||||
self.write("CAP LS")
|
||||
self.write("NICK :" + self.nick)
|
||||
self.write("USER %s %s bla :%s" % (self.username, socket.getfqdn(), self.realname))
|
||||
|
||||
|
||||
def close(self):
|
||||
if not self._closed:
|
||||
self.write("QUIT")
|
||||
return super().close()
|
||||
|
||||
|
||||
# Writes: as inherited
|
||||
|
||||
# Read
|
||||
|
||||
def async_read(self):
|
||||
for line in super().async_read():
|
||||
# PING should be handled here, so start parsing here :/
|
||||
msg = IRCMessage(line, self.encoding)
|
||||
|
||||
if msg.cmd in self.hookscmd:
|
||||
self.hookscmd[msg.cmd](msg)
|
||||
|
||||
yield msg
|
||||
|
||||
|
||||
def parse(self, msg):
|
||||
mes = msg.to_bot_message(self)
|
||||
if mes is not None:
|
||||
yield mes
|
||||
|
||||
|
||||
def subparse(self, orig, cnt):
|
||||
msg = IRCMessage(("@time=%s :%s!user@host.com PRIVMSG %s :%s" % (orig.date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), orig.frm, ",".join(orig.to), cnt)).encode(self.encoding), self.encoding)
|
||||
return msg.to_bot_message(self)
|
||||
|
||||
|
||||
class IRC(_IRC, SocketServer):
|
||||
pass
|
||||
|
||||
class IRC_secure(_IRC, SecureSocketServer):
|
||||
pass
|
||||
|
|
@ -1,375 +0,0 @@
|
|||
# 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 datetime import datetime
|
||||
import shlex
|
||||
import threading
|
||||
|
||||
import irc.bot
|
||||
import irc.client
|
||||
import irc.connection
|
||||
|
||||
import nemubot.message as message
|
||||
from nemubot.server.threaded import ThreadedServer
|
||||
|
||||
|
||||
class _IRCBotAdapter(irc.bot.SingleServerIRCBot):
|
||||
|
||||
"""Internal adapter that bridges the irc library event model to nemubot.
|
||||
|
||||
Subclasses SingleServerIRCBot to get automatic reconnection, PING/PONG,
|
||||
and nick-collision handling for free.
|
||||
"""
|
||||
|
||||
def __init__(self, server_name, push_fn, channels, on_connect_cmds,
|
||||
nick, server_list, owner=None, realname="Nemubot",
|
||||
encoding="utf-8", **connect_params):
|
||||
super().__init__(server_list, nick, realname, **connect_params)
|
||||
self._nemubot_name = server_name
|
||||
self._push = push_fn
|
||||
self._channels_to_join = channels
|
||||
self._on_connect_cmds = on_connect_cmds or []
|
||||
self.owner = owner
|
||||
self.encoding = encoding
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
|
||||
# Event loop control
|
||||
|
||||
def start(self):
|
||||
"""Run the reactor loop until stop() is called."""
|
||||
self._connect()
|
||||
while not self._stop_event.is_set():
|
||||
self.reactor.process_once(timeout=0.2)
|
||||
|
||||
def stop(self):
|
||||
"""Signal the loop to exit and disconnect cleanly."""
|
||||
self._stop_event.set()
|
||||
try:
|
||||
self.connection.disconnect("Goodbye")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_disconnect(self, connection, event):
|
||||
"""Reconnect automatically unless we are shutting down."""
|
||||
if not self._stop_event.is_set():
|
||||
super().on_disconnect(connection, event)
|
||||
|
||||
|
||||
# Connection lifecycle
|
||||
|
||||
def on_welcome(self, connection, event):
|
||||
"""001 — run on_connect commands then join channels."""
|
||||
for cmd in self._on_connect_cmds:
|
||||
if callable(cmd):
|
||||
for c in (cmd() or []):
|
||||
connection.send_raw(c)
|
||||
else:
|
||||
connection.send_raw(cmd)
|
||||
|
||||
for ch in self._channels_to_join:
|
||||
if isinstance(ch, tuple):
|
||||
connection.join(ch[0], ch[1] if len(ch) > 1 else "")
|
||||
elif hasattr(ch, 'name'):
|
||||
connection.join(ch.name, getattr(ch, 'password', "") or "")
|
||||
else:
|
||||
connection.join(str(ch))
|
||||
|
||||
def on_invite(self, connection, event):
|
||||
"""Auto-join on INVITE."""
|
||||
if event.arguments:
|
||||
connection.join(event.arguments[0])
|
||||
|
||||
|
||||
# CTCP
|
||||
|
||||
def on_ctcp(self, connection, event):
|
||||
"""Handle CTCP requests (irc library >= 19 dispatches all to on_ctcp)."""
|
||||
nick = irc.client.NickMask(event.source).nick
|
||||
ctcp_type = event.arguments[0].upper() if event.arguments else ""
|
||||
ctcp_arg = event.arguments[1] if len(event.arguments) > 1 else ""
|
||||
self._reply_ctcp(connection, nick, ctcp_type, ctcp_arg)
|
||||
|
||||
# Fallbacks for older irc library versions that dispatch per-type
|
||||
def on_ctcpversion(self, connection, event):
|
||||
import nemubot
|
||||
nick = irc.client.NickMask(event.source).nick
|
||||
connection.ctcp_reply(nick, "VERSION nemubot v%s" % nemubot.__version__)
|
||||
|
||||
def on_ctcpping(self, connection, event):
|
||||
nick = irc.client.NickMask(event.source).nick
|
||||
arg = event.arguments[0] if event.arguments else ""
|
||||
connection.ctcp_reply(nick, "PING %s" % arg)
|
||||
|
||||
def _reply_ctcp(self, connection, nick, ctcp_type, ctcp_arg):
|
||||
import nemubot
|
||||
responses = {
|
||||
"ACTION": None, # handled as on_action
|
||||
"CLIENTINFO": "CLIENTINFO ACTION CLIENTINFO FINGER PING SOURCE TIME USERINFO VERSION",
|
||||
"FINGER": "FINGER nemubot v%s" % nemubot.__version__,
|
||||
"PING": "PING %s" % ctcp_arg,
|
||||
"SOURCE": "SOURCE https://github.com/nemunaire/nemubot",
|
||||
"TIME": "TIME %s" % datetime.now(),
|
||||
"USERINFO": "USERINFO Nemubot",
|
||||
"VERSION": "VERSION nemubot v%s" % nemubot.__version__,
|
||||
}
|
||||
if ctcp_type in responses and responses[ctcp_type] is not None:
|
||||
connection.ctcp_reply(nick, responses[ctcp_type])
|
||||
|
||||
|
||||
# Incoming messages
|
||||
|
||||
def _decode(self, text):
|
||||
if isinstance(text, bytes):
|
||||
try:
|
||||
return text.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return text.decode(self.encoding, "replace")
|
||||
return text
|
||||
|
||||
def _make_message(self, connection, source, target, text):
|
||||
"""Convert raw IRC event data into a nemubot bot message."""
|
||||
nick = irc.client.NickMask(source).nick
|
||||
text = self._decode(text)
|
||||
bot_nick = connection.get_nickname()
|
||||
is_channel = irc.client.is_channel(target)
|
||||
to = [target] if is_channel else [nick]
|
||||
to_response = [target] if is_channel else [nick]
|
||||
|
||||
common = dict(
|
||||
server=self._nemubot_name,
|
||||
to=to,
|
||||
to_response=to_response,
|
||||
frm=nick,
|
||||
frm_owner=(nick == self.owner),
|
||||
)
|
||||
|
||||
# "botname: text" or "botname, text"
|
||||
if (text.startswith(bot_nick + ":") or
|
||||
text.startswith(bot_nick + ",")):
|
||||
inner = text[len(bot_nick) + 1:].strip()
|
||||
return message.DirectAsk(designated=bot_nick, message=inner,
|
||||
**common)
|
||||
|
||||
# "!command [args]"
|
||||
if len(text) > 1 and text[0] == '!':
|
||||
inner = text[1:].strip()
|
||||
try:
|
||||
args = shlex.split(inner)
|
||||
except ValueError:
|
||||
args = inner.split()
|
||||
if args:
|
||||
# Extract @key=value named arguments (same logic as IRC.py)
|
||||
kwargs = {}
|
||||
while len(args) > 1:
|
||||
arg = args[1]
|
||||
if len(arg) > 2 and arg[0:2] == '\\@':
|
||||
args[1] = arg[1:]
|
||||
elif len(arg) > 1 and arg[0] == '@':
|
||||
arsp = arg[1:].split("=", 1)
|
||||
kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
|
||||
args.pop(1)
|
||||
continue
|
||||
break
|
||||
return message.Command(cmd=args[0], args=args[1:],
|
||||
kwargs=kwargs, **common)
|
||||
|
||||
return message.Text(message=text, **common)
|
||||
|
||||
def on_pubmsg(self, connection, event):
|
||||
msg = self._make_message(
|
||||
connection, event.source, event.target,
|
||||
event.arguments[0] if event.arguments else "",
|
||||
)
|
||||
if msg:
|
||||
self._push(msg)
|
||||
|
||||
def on_privmsg(self, connection, event):
|
||||
nick = irc.client.NickMask(event.source).nick
|
||||
msg = self._make_message(
|
||||
connection, event.source, nick,
|
||||
event.arguments[0] if event.arguments else "",
|
||||
)
|
||||
if msg:
|
||||
self._push(msg)
|
||||
|
||||
def on_action(self, connection, event):
|
||||
"""CTCP ACTION (/me) — delivered as a plain Text message."""
|
||||
nick = irc.client.NickMask(event.source).nick
|
||||
text = "/me %s" % (event.arguments[0] if event.arguments else "")
|
||||
is_channel = irc.client.is_channel(event.target)
|
||||
to = [event.target] if is_channel else [nick]
|
||||
self._push(message.Text(
|
||||
message=text,
|
||||
server=self._nemubot_name,
|
||||
to=to, to_response=to,
|
||||
frm=nick, frm_owner=(nick == self.owner),
|
||||
))
|
||||
|
||||
|
||||
class IRCLib(ThreadedServer):
|
||||
|
||||
"""IRC server using the irc Python library (jaraco).
|
||||
|
||||
Compared to the hand-rolled IRC.py implementation, this gets:
|
||||
- Automatic exponential-backoff reconnection
|
||||
- PING/PONG handled transparently
|
||||
- Nick-collision suffix logic built-in
|
||||
"""
|
||||
|
||||
def __init__(self, host="localhost", port=6667, nick="nemubot",
|
||||
username=None, password=None, realname="Nemubot",
|
||||
encoding="utf-8", owner=None, channels=None,
|
||||
on_connect=None, ssl=False, **kwargs):
|
||||
"""Prepare a connection to an IRC server.
|
||||
|
||||
Keyword arguments:
|
||||
host -- IRC server hostname
|
||||
port -- IRC server port (default 6667)
|
||||
nick -- bot's nickname
|
||||
username -- username for USER command (defaults to nick)
|
||||
password -- server password (sent as PASS)
|
||||
realname -- bot's real name
|
||||
encoding -- fallback encoding for non-UTF-8 servers
|
||||
owner -- nick of the bot's owner (sets frm_owner on messages)
|
||||
channels -- list of channel names / (name, key) tuples to join
|
||||
on_connect -- list of raw IRC commands (or a callable returning one)
|
||||
to send after receiving 001
|
||||
ssl -- wrap the connection in TLS
|
||||
"""
|
||||
name = (username or nick) + "@" + host + ":" + str(port)
|
||||
super().__init__(name=name)
|
||||
|
||||
self._host = host
|
||||
self._port = int(port)
|
||||
self._nick = nick
|
||||
self._username = username or nick
|
||||
self._password = password
|
||||
self._realname = realname
|
||||
self._encoding = encoding
|
||||
self.owner = owner
|
||||
self._channels = channels or []
|
||||
self._on_connect_cmds = on_connect
|
||||
self._ssl = ssl
|
||||
|
||||
self._bot = None
|
||||
self._thread = None
|
||||
|
||||
|
||||
# ThreadedServer hooks
|
||||
|
||||
def _start(self):
|
||||
server_list = [irc.bot.ServerSpec(self._host, self._port,
|
||||
self._password)]
|
||||
|
||||
connect_params = {"username": self._username}
|
||||
|
||||
if self._ssl:
|
||||
import ssl as ssl_mod
|
||||
ctx = ssl_mod.create_default_context()
|
||||
host = self._host # capture for closure
|
||||
connect_params["connect_factory"] = irc.connection.Factory(
|
||||
wrapper=lambda sock: ctx.wrap_socket(sock,
|
||||
server_hostname=host)
|
||||
)
|
||||
|
||||
self._bot = _IRCBotAdapter(
|
||||
server_name=self.name,
|
||||
push_fn=self._push_message,
|
||||
channels=self._channels,
|
||||
on_connect_cmds=self._on_connect_cmds,
|
||||
nick=self._nick,
|
||||
server_list=server_list,
|
||||
owner=self.owner,
|
||||
realname=self._realname,
|
||||
encoding=self._encoding,
|
||||
**connect_params,
|
||||
)
|
||||
self._thread = threading.Thread(
|
||||
target=self._bot.start,
|
||||
daemon=True,
|
||||
name="nemubot.IRC/" + self.name,
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
def _stop(self):
|
||||
if self._bot:
|
||||
self._bot.stop()
|
||||
if self._thread:
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
|
||||
# Outgoing messages
|
||||
|
||||
def send_response(self, response):
|
||||
if response is None:
|
||||
return
|
||||
if isinstance(response, list):
|
||||
for r in response:
|
||||
self.send_response(r)
|
||||
return
|
||||
if not self._bot:
|
||||
return
|
||||
|
||||
from nemubot.message.printer.IRCLib import IRCLib as IRCLibPrinter
|
||||
printer = IRCLibPrinter(self._bot.connection)
|
||||
response.accept(printer)
|
||||
|
||||
|
||||
# subparse: re-parse a plain string in the context of an existing message
|
||||
# (used by alias, rnd, grep, cat, smmry, sms modules)
|
||||
|
||||
def subparse(self, orig, cnt):
|
||||
bot_nick = (self._bot.connection.get_nickname()
|
||||
if self._bot else self._nick)
|
||||
common = dict(
|
||||
server=self.name,
|
||||
to=orig.to,
|
||||
to_response=orig.to_response,
|
||||
frm=orig.frm,
|
||||
frm_owner=orig.frm_owner,
|
||||
date=orig.date,
|
||||
)
|
||||
text = cnt
|
||||
|
||||
if (text.startswith(bot_nick + ":") or
|
||||
text.startswith(bot_nick + ",")):
|
||||
inner = text[len(bot_nick) + 1:].strip()
|
||||
return message.DirectAsk(designated=bot_nick, message=inner,
|
||||
**common)
|
||||
|
||||
if len(text) > 1 and text[0] == '!':
|
||||
inner = text[1:].strip()
|
||||
try:
|
||||
args = shlex.split(inner)
|
||||
except ValueError:
|
||||
args = inner.split()
|
||||
if args:
|
||||
kwargs = {}
|
||||
while len(args) > 1:
|
||||
arg = args[1]
|
||||
if len(arg) > 2 and arg[0:2] == '\\@':
|
||||
args[1] = arg[1:]
|
||||
elif len(arg) > 1 and arg[0] == '@':
|
||||
arsp = arg[1:].split("=", 1)
|
||||
kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
|
||||
args.pop(1)
|
||||
continue
|
||||
break
|
||||
return message.Command(cmd=args[0], args=args[1:],
|
||||
kwargs=kwargs, **common)
|
||||
|
||||
return message.Text(message=text, **common)
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
# 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/>.
|
||||
|
||||
import asyncio
|
||||
import shlex
|
||||
import threading
|
||||
|
||||
import nemubot.message as message
|
||||
from nemubot.server.threaded import ThreadedServer
|
||||
|
||||
|
||||
class Matrix(ThreadedServer):
|
||||
|
||||
"""Matrix server implementation using matrix-nio's AsyncClient.
|
||||
|
||||
Runs an asyncio event loop in a daemon thread. Incoming room messages are
|
||||
converted to nemubot bot messages and pushed through the pipe; outgoing
|
||||
responses are sent via the async client from the same event loop.
|
||||
"""
|
||||
|
||||
def __init__(self, homeserver, user_id, password=None, access_token=None,
|
||||
owner=None, nick=None, channels=None, **kwargs):
|
||||
"""Prepare a connection to a Matrix homeserver.
|
||||
|
||||
Keyword arguments:
|
||||
homeserver -- base URL of the homeserver, e.g. "https://matrix.org"
|
||||
user_id -- full MXID (@user:server) or bare localpart
|
||||
password -- login password (required if no access_token)
|
||||
access_token -- pre-obtained access token (alternative to password)
|
||||
owner -- MXID of the bot owner (marks frm_owner on messages)
|
||||
nick -- display name / prefix for DirectAsk detection
|
||||
channels -- list of room IDs / aliases to join on connect
|
||||
"""
|
||||
|
||||
# Ensure fully-qualified MXID
|
||||
if not user_id.startswith("@"):
|
||||
host = homeserver.split("//")[-1].rstrip("/")
|
||||
user_id = "@%s:%s" % (user_id, host)
|
||||
|
||||
super().__init__(name=user_id)
|
||||
|
||||
self.homeserver = homeserver
|
||||
self.user_id = user_id
|
||||
self.password = password
|
||||
self.access_token = access_token
|
||||
self.owner = owner
|
||||
self.nick = nick or user_id
|
||||
|
||||
self._initial_rooms = channels or []
|
||||
self._client = None
|
||||
self._loop = None
|
||||
self._thread = None
|
||||
|
||||
|
||||
# Open/close
|
||||
|
||||
def _start(self):
|
||||
self._thread = threading.Thread(
|
||||
target=self._run_loop,
|
||||
daemon=True,
|
||||
name="nemubot.Matrix/" + self._name,
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
def _stop(self):
|
||||
if self._client and self._loop and not self._loop.is_closed():
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._client.close(), self._loop
|
||||
).result(timeout=5)
|
||||
except Exception:
|
||||
self._logger.exception("Error while closing Matrix client")
|
||||
if self._thread:
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
|
||||
# Asyncio thread
|
||||
|
||||
def _run_loop(self):
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
try:
|
||||
self._loop.run_until_complete(self._async_main())
|
||||
except Exception:
|
||||
self._logger.exception("Unhandled exception in Matrix event loop")
|
||||
finally:
|
||||
self._loop.close()
|
||||
|
||||
async def _async_main(self):
|
||||
from nio import AsyncClient, LoginError, RoomMessageText
|
||||
|
||||
self._client = AsyncClient(self.homeserver, self.user_id)
|
||||
|
||||
if self.access_token:
|
||||
self._client.access_token = self.access_token
|
||||
self._logger.info("Using provided access token for %s", self.user_id)
|
||||
elif self.password:
|
||||
resp = await self._client.login(self.password)
|
||||
if isinstance(resp, LoginError):
|
||||
self._logger.error("Matrix login failed: %s", resp.message)
|
||||
return
|
||||
self._logger.info("Logged in to Matrix as %s", self.user_id)
|
||||
else:
|
||||
self._logger.error("Need either password or access_token to connect")
|
||||
return
|
||||
|
||||
self._client.add_event_callback(self._on_room_message, RoomMessageText)
|
||||
|
||||
for room in self._initial_rooms:
|
||||
await self._client.join(room)
|
||||
self._logger.info("Joined room %s", room)
|
||||
|
||||
await self._client.sync_forever(timeout=30000, full_state=True)
|
||||
|
||||
|
||||
# Incoming messages
|
||||
|
||||
async def _on_room_message(self, room, event):
|
||||
"""Callback invoked by matrix-nio for each m.room.message event."""
|
||||
|
||||
if event.sender == self.user_id:
|
||||
return # ignore own messages
|
||||
|
||||
text = event.body
|
||||
room_id = room.room_id
|
||||
frm = event.sender
|
||||
|
||||
common_args = {
|
||||
"server": self.name,
|
||||
"to": [room_id],
|
||||
"to_response": [room_id],
|
||||
"frm": frm,
|
||||
"frm_owner": frm == self.owner,
|
||||
}
|
||||
|
||||
if len(text) > 1 and text[0] == '!':
|
||||
text = text[1:].strip()
|
||||
try:
|
||||
args = shlex.split(text)
|
||||
except ValueError:
|
||||
args = text.split(' ')
|
||||
msg = message.Command(cmd=args[0], args=args[1:], **common_args)
|
||||
|
||||
elif (text.lower().startswith(self.nick.lower() + ":")
|
||||
or text.lower().startswith(self.nick.lower() + ",")):
|
||||
text = text[len(self.nick) + 1:].strip()
|
||||
msg = message.DirectAsk(designated=self.nick, message=text,
|
||||
**common_args)
|
||||
|
||||
else:
|
||||
msg = message.Text(message=text, **common_args)
|
||||
|
||||
self._push_message(msg)
|
||||
|
||||
|
||||
# Outgoing messages
|
||||
|
||||
def send_response(self, response):
|
||||
if response is None:
|
||||
return
|
||||
if isinstance(response, list):
|
||||
for r in response:
|
||||
self.send_response(r)
|
||||
return
|
||||
|
||||
from nemubot.message.printer.Matrix import Matrix as MatrixPrinter
|
||||
printer = MatrixPrinter(self._send_text)
|
||||
response.accept(printer)
|
||||
|
||||
def _send_text(self, room_id, text):
|
||||
"""Thread-safe: schedule a Matrix room_send on the asyncio loop."""
|
||||
if not self._client or not self._loop or self._loop.is_closed():
|
||||
self._logger.warning("Cannot send: Matrix client not ready")
|
||||
return
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self._client.room_send(
|
||||
room_id=room_id,
|
||||
message_type="m.room.message",
|
||||
content={"msgtype": "m.text", "body": text},
|
||||
ignore_unverified_devices=True,
|
||||
),
|
||||
self._loop,
|
||||
)
|
||||
future.add_done_callback(
|
||||
lambda f: self._logger.warning("Matrix send error: %s", f.exception())
|
||||
if not f.cancelled() and f.exception() else None
|
||||
)
|
||||
|
|
@ -22,15 +22,25 @@ def factory(uri, ssl=False, **init_args):
|
|||
srv = None
|
||||
|
||||
if o.scheme == "irc" or o.scheme == "ircs":
|
||||
# https://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt
|
||||
# https://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html
|
||||
args = dict(init_args)
|
||||
# http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt
|
||||
# http://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html
|
||||
args = init_args
|
||||
|
||||
if o.scheme == "ircs": ssl = True
|
||||
if o.hostname is not None: args["host"] = o.hostname
|
||||
if o.port is not None: args["port"] = o.port
|
||||
if o.username is not None: args["username"] = o.username
|
||||
if o.password is not None: args["password"] = unquote(o.password)
|
||||
if o.password is not None: args["password"] = o.password
|
||||
|
||||
if ssl:
|
||||
try:
|
||||
from ssl import create_default_context
|
||||
args["_context"] = create_default_context()
|
||||
except ImportError:
|
||||
# Python 3.3 compat
|
||||
from ssl import SSLContext, PROTOCOL_TLSv1
|
||||
args["_context"] = SSLContext(PROTOCOL_TLSv1)
|
||||
args["server_hostname"] = o.hostname
|
||||
|
||||
modifiers = o.path.split(",")
|
||||
target = unquote(modifiers.pop(0)[1:])
|
||||
|
|
@ -41,58 +51,28 @@ def factory(uri, ssl=False, **init_args):
|
|||
if "msg" in params:
|
||||
if "on_connect" not in args:
|
||||
args["on_connect"] = []
|
||||
args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"][0]))
|
||||
args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"]))
|
||||
|
||||
if "key" in params:
|
||||
if "channels" not in args:
|
||||
args["channels"] = []
|
||||
args["channels"].append((target, params["key"][0]))
|
||||
args["channels"].append((target, params["key"]))
|
||||
|
||||
if "pass" in params:
|
||||
args["password"] = params["pass"][0]
|
||||
args["password"] = params["pass"]
|
||||
|
||||
if "charset" in params:
|
||||
args["encoding"] = params["charset"][0]
|
||||
args["encoding"] = params["charset"]
|
||||
|
||||
#
|
||||
if "channels" not in args and "isnick" not in modifiers:
|
||||
args["channels"] = [target]
|
||||
args["channels"] = [ target ]
|
||||
|
||||
args["ssl"] = ssl
|
||||
|
||||
from nemubot.server.IRCLib import IRCLib as IRCServer
|
||||
srv = IRCServer(**args)
|
||||
|
||||
elif o.scheme == "matrix":
|
||||
# matrix://localpart:password@homeserver.tld/!room:homeserver.tld
|
||||
# matrix://localpart:password@homeserver.tld/%23alias:homeserver.tld
|
||||
# Use matrixs:// for https (default) vs http
|
||||
args = dict(init_args)
|
||||
|
||||
homeserver = "https://" + o.hostname
|
||||
if o.port is not None:
|
||||
homeserver += ":%d" % o.port
|
||||
args["homeserver"] = homeserver
|
||||
|
||||
if o.username is not None:
|
||||
args["user_id"] = o.username
|
||||
if o.password is not None:
|
||||
args["password"] = unquote(o.password)
|
||||
|
||||
# Parse rooms from path (comma-separated, URL-encoded)
|
||||
if o.path and o.path != "/":
|
||||
rooms = [unquote(r) for r in o.path.lstrip("/").split(",") if r]
|
||||
if rooms:
|
||||
args.setdefault("channels", []).extend(rooms)
|
||||
|
||||
params = parse_qs(o.query)
|
||||
if "token" in params:
|
||||
args["access_token"] = params["token"][0]
|
||||
if "nick" in params:
|
||||
args["nick"] = params["nick"][0]
|
||||
if "owner" in params:
|
||||
args["owner"] = params["owner"][0]
|
||||
|
||||
from nemubot.server.Matrix import Matrix as MatrixServer
|
||||
srv = MatrixServer(**args)
|
||||
if ssl:
|
||||
from nemubot.server.IRC import IRC_secure as SecureIRCServer
|
||||
srv = SecureIRCServer(**args)
|
||||
else:
|
||||
from nemubot.server.IRC import IRC as IRCServer
|
||||
srv = IRCServer(**args)
|
||||
|
||||
return srv
|
||||
|
|
|
|||
|
|
@ -25,18 +25,18 @@ class AbstractServer:
|
|||
|
||||
"""An abstract server: handle communication with an IM server"""
|
||||
|
||||
def __init__(self, name, fdClass, **kwargs):
|
||||
def __init__(self, name=None, **kwargs):
|
||||
"""Initialize an abstract server
|
||||
|
||||
Keyword argument:
|
||||
name -- Identifier of the socket, for convinience
|
||||
fdClass -- Class to instantiate as support file
|
||||
"""
|
||||
|
||||
self._name = name
|
||||
self._fd = fdClass(**kwargs)
|
||||
|
||||
self._logger = logging.getLogger("nemubot.server." + str(self.name))
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.logger = logging.getLogger("nemubot.server." + str(self.name))
|
||||
self._readbuffer = b''
|
||||
self._sending_queue = queue.Queue()
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ class AbstractServer:
|
|||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return self._fd.fileno()
|
||||
return self.fileno()
|
||||
|
||||
|
||||
# Open/close
|
||||
|
|
@ -54,25 +54,25 @@ class AbstractServer:
|
|||
def connect(self, *args, **kwargs):
|
||||
"""Register the server in _poll"""
|
||||
|
||||
self._logger.info("Opening connection")
|
||||
self.logger.info("Opening connection")
|
||||
|
||||
self._fd.connect(*args, **kwargs)
|
||||
super().connect(*args, **kwargs)
|
||||
|
||||
self._on_connect()
|
||||
|
||||
def _on_connect(self):
|
||||
sync_act("sckt", "register", self._fd.fileno())
|
||||
sync_act("sckt", "register", self.fileno())
|
||||
|
||||
|
||||
def close(self, *args, **kwargs):
|
||||
"""Unregister the server from _poll"""
|
||||
|
||||
self._logger.info("Closing connection")
|
||||
self.logger.info("Closing connection")
|
||||
|
||||
if self._fd.fileno() > 0:
|
||||
sync_act("sckt", "unregister", self._fd.fileno())
|
||||
if self.fileno() > 0:
|
||||
sync_act("sckt", "unregister", self.fileno())
|
||||
|
||||
self._fd.close(*args, **kwargs)
|
||||
super().close(*args, **kwargs)
|
||||
|
||||
|
||||
# Writes
|
||||
|
|
@ -85,15 +85,15 @@ class AbstractServer:
|
|||
"""
|
||||
|
||||
self._sending_queue.put(self.format(message))
|
||||
self._logger.debug("Message '%s' appended to write queue coming from %s:%d in %s", message, *traceback.extract_stack(limit=3)[0][:3])
|
||||
sync_act("sckt", "write", self._fd.fileno())
|
||||
self.logger.debug("Message '%s' appended to write queue coming from %s:%d in %s", message, *traceback.extract_stack(limit=3)[0][:3])
|
||||
sync_act("sckt", "write", self.fileno())
|
||||
|
||||
|
||||
def async_write(self):
|
||||
"""Internal function used when the file descriptor is writable"""
|
||||
|
||||
try:
|
||||
sync_act("sckt", "unwrite", self._fd.fileno())
|
||||
sync_act("sckt", "unwrite", self.fileno())
|
||||
while not self._sending_queue.empty():
|
||||
self._write(self._sending_queue.get_nowait())
|
||||
self._sending_queue.task_done()
|
||||
|
|
@ -159,9 +159,4 @@ class AbstractServer:
|
|||
def exception(self, flags):
|
||||
"""Exception occurs on fd"""
|
||||
|
||||
self._fd.close()
|
||||
|
||||
# Proxy
|
||||
|
||||
def fileno(self):
|
||||
return self._fd.fileno()
|
||||
self.close()
|
||||
|
|
|
|||
52
nemubot/server/factory_test.py
Normal file
52
nemubot/server/factory_test.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# 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 unittest
|
||||
|
||||
from nemubot.server import factory
|
||||
|
||||
class TestFactory(unittest.TestCase):
|
||||
|
||||
def test_IRC1(self):
|
||||
from nemubot.server.IRC import IRC as IRCServer
|
||||
from nemubot.server.IRC import IRC_secure as IRCSServer
|
||||
|
||||
# <host>: If omitted, the client must connect to a prespecified default IRC server.
|
||||
server = factory("irc:///")
|
||||
self.assertIsInstance(server, IRCServer)
|
||||
self.assertEqual(server.host, "localhost")
|
||||
|
||||
server = factory("ircs:///")
|
||||
self.assertIsInstance(server, IRCSServer)
|
||||
self.assertEqual(server.host, "localhost")
|
||||
|
||||
server = factory("irc://host1")
|
||||
self.assertIsInstance(server, IRCServer)
|
||||
self.assertEqual(server.host, "host1")
|
||||
|
||||
server = factory("irc://host2:6667")
|
||||
self.assertIsInstance(server, IRCServer)
|
||||
self.assertEqual(server.host, "host2")
|
||||
self.assertEqual(server.port, 6667)
|
||||
|
||||
server = factory("ircs://host3:194/")
|
||||
self.assertIsInstance(server, IRCSServer)
|
||||
self.assertEqual(server.host, "host3")
|
||||
self.assertEqual(server.port, 194)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
210
nemubot/server/message/IRC.py
Normal file
210
nemubot/server/message/IRC.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# 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 datetime import datetime, timezone
|
||||
import re
|
||||
import shlex
|
||||
|
||||
import nemubot.message as message
|
||||
from nemubot.server.message.abstract import Abstract
|
||||
|
||||
mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )?
|
||||
(?::(?P<prefix>
|
||||
(?P<nick>[^!@ ]+)
|
||||
(?: !(?P<user>[^@ ]+))?
|
||||
(?:@(?P<host>[^ ]*))?
|
||||
)\ )?
|
||||
(?P<command>(?:[a-zA-Z]+|[0-9]{3}))
|
||||
(?P<params>(?:\ [^:][^ ]*)*)(?:\ :(?P<trailing>.*))?
|
||||
$''', re.X)
|
||||
|
||||
class IRC(Abstract):
|
||||
|
||||
"""Class responsible for parsing IRC messages"""
|
||||
|
||||
def __init__(self, raw, encoding="utf-8"):
|
||||
self.encoding = encoding
|
||||
self.tags = { 'time': datetime.now(timezone.utc) }
|
||||
self.params = list()
|
||||
|
||||
p = mgx.match(raw.rstrip())
|
||||
|
||||
if p is None:
|
||||
raise Exception("Not a valid IRC message: %s" % raw)
|
||||
|
||||
# Parse tags if exists: @aaa=bbb;ccc;example.com/ddd=eee
|
||||
if p.group("tags"):
|
||||
for tgs in self.decode(p.group("tags")).split(';'):
|
||||
tag = tgs.split('=')
|
||||
if len(tag) > 1:
|
||||
self.add_tag(tag[0], tag[1])
|
||||
else:
|
||||
self.add_tag(tag[0])
|
||||
|
||||
# Parse prefix if exists: :nick!user@host.com
|
||||
self.prefix = self.decode(p.group("prefix"))
|
||||
self.nick = self.decode(p.group("nick"))
|
||||
self.user = self.decode(p.group("user"))
|
||||
self.host = self.decode(p.group("host"))
|
||||
|
||||
# Parse command
|
||||
self.cmd = self.decode(p.group("command"))
|
||||
|
||||
# Parse params
|
||||
if p.group("params") is not None and p.group("params") != b'':
|
||||
for param in p.group("params").strip().split(b' '):
|
||||
self.params.append(param)
|
||||
|
||||
if p.group("trailing") is not None:
|
||||
self.params.append(p.group("trailing"))
|
||||
|
||||
|
||||
def add_tag(self, key, value=None):
|
||||
"""Add an IRCv3.2 Message Tags
|
||||
|
||||
Arguments:
|
||||
key -- tag identifier (unique for the message)
|
||||
value -- optional value for the tag
|
||||
"""
|
||||
|
||||
# Treat special tags
|
||||
if key == "time" and value is not None:
|
||||
import calendar, time
|
||||
value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc)
|
||||
|
||||
# Store tag
|
||||
self.tags[key] = value
|
||||
|
||||
|
||||
@property
|
||||
def is_ctcp(self):
|
||||
"""Analyze a message, to determine if this is a CTCP one"""
|
||||
return self.cmd == "PRIVMSG" and len(self.params) == 2 and len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01)
|
||||
|
||||
|
||||
def decode(self, s):
|
||||
"""Decode the content string usign a specific encoding
|
||||
|
||||
Argument:
|
||||
s -- string to decode
|
||||
"""
|
||||
|
||||
if isinstance(s, bytes):
|
||||
try:
|
||||
s = s.decode()
|
||||
except UnicodeDecodeError:
|
||||
s = s.decode(self.encoding, 'replace')
|
||||
return s
|
||||
|
||||
|
||||
|
||||
def to_server_string(self, client=True):
|
||||
"""Pretty print the message to close to original input string
|
||||
|
||||
Keyword argument:
|
||||
client -- export as a client-side string if true
|
||||
"""
|
||||
|
||||
res = ";".join(["@%s=%s" % (k, v if not isinstance(v, datetime) else v.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) for k, v in self.tags.items()])
|
||||
|
||||
if not client:
|
||||
res += " :%s!%s@%s" % (self.nick, self.user, self.host)
|
||||
|
||||
res += " " + self.cmd
|
||||
|
||||
if len(self.params) > 0:
|
||||
|
||||
if len(self.params) > 1:
|
||||
res += " " + self.decode(b" ".join(self.params[:-1]))
|
||||
res += " :" + self.decode(self.params[-1])
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def to_bot_message(self, srv):
|
||||
"""Convert to one of concrete implementation of AbstractMessage
|
||||
|
||||
Argument:
|
||||
srv -- the server from the message was received
|
||||
"""
|
||||
|
||||
if self.cmd == "PRIVMSG" or self.cmd == "NOTICE":
|
||||
|
||||
receivers = self.decode(self.params[0]).split(',')
|
||||
|
||||
common_args = {
|
||||
"server": srv.name,
|
||||
"date": self.tags["time"],
|
||||
"to": receivers,
|
||||
"to_response": [r if r != srv.nick else self.nick for r in receivers],
|
||||
"frm": self.nick,
|
||||
"frm_owner": self.nick == srv.owner
|
||||
}
|
||||
|
||||
# If CTCP, remove 0x01
|
||||
if self.is_ctcp:
|
||||
text = self.decode(self.params[1][1:len(self.params[1])-1])
|
||||
else:
|
||||
text = self.decode(self.params[1])
|
||||
|
||||
if text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":":
|
||||
designated = srv.nick
|
||||
text = text[len(srv.nick) + 1:].strip()
|
||||
else:
|
||||
designated = None
|
||||
|
||||
# Is this a command?
|
||||
if len(text) > 1 and text[0] == '!':
|
||||
text = text[1:].strip()
|
||||
|
||||
# Split content by words
|
||||
try:
|
||||
args = shlex.split(text)
|
||||
except ValueError:
|
||||
args = text.split(' ')
|
||||
|
||||
# Extract explicit named arguments: @key=value or just @key, only at begening
|
||||
kwargs = {}
|
||||
while len(args) > 1:
|
||||
arg = args[1]
|
||||
if len(arg) > 2:
|
||||
if arg[0:2] == '\\@':
|
||||
args[1] = arg[1:]
|
||||
elif arg[0] == '@':
|
||||
arsp = arg[1:].split("=", 1)
|
||||
if len(arsp) == 2:
|
||||
kwargs[arsp[0]] = arsp[1]
|
||||
else:
|
||||
kwargs[arg[1:]] = None
|
||||
args.pop(1)
|
||||
continue
|
||||
# Futher argument are considered as normal argument (this helps for subcommand treatment)
|
||||
break
|
||||
|
||||
return message.Command(cmd=args[0],
|
||||
args=args[1:],
|
||||
kwargs=kwargs,
|
||||
**common_args)
|
||||
|
||||
# Is this an ask for this bot?
|
||||
elif designated is not None:
|
||||
return message.DirectAsk(designated=designated, message=text, **common_args)
|
||||
|
||||
# Normal message
|
||||
else:
|
||||
return message.Text(message=text, **common_args)
|
||||
|
||||
return None
|
||||
15
nemubot/server/message/__init__.py
Normal file
15
nemubot/server/message/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# 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/>.
|
||||
33
nemubot/server/message/abstract.py
Normal file
33
nemubot/server/message/abstract.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# 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/>.
|
||||
|
||||
class Abstract:
|
||||
|
||||
def to_bot_message(self, srv):
|
||||
"""Convert to one of concrete implementation of AbstractMessage
|
||||
|
||||
Argument:
|
||||
srv -- the server from the message was received
|
||||
"""
|
||||
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
def to_server_string(self, **kwargs):
|
||||
"""Pretty print the message to close to original input string
|
||||
"""
|
||||
|
||||
raise NotImplemented
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
import nemubot.message as message
|
||||
from nemubot.message.printer.socket import Socket as SocketPrinter
|
||||
|
|
@ -39,7 +40,7 @@ class _Socket(AbstractServer):
|
|||
# Write
|
||||
|
||||
def _write(self, cnt):
|
||||
self._fd.sendall(cnt)
|
||||
self.sendall(cnt)
|
||||
|
||||
|
||||
def format(self, txt):
|
||||
|
|
@ -51,8 +52,8 @@ class _Socket(AbstractServer):
|
|||
|
||||
# Read
|
||||
|
||||
def read(self, bufsize=1024, *args, **kwargs):
|
||||
return self._fd.recv(bufsize, *args, **kwargs)
|
||||
def recv(self, n=1024):
|
||||
return super().recv(n)
|
||||
|
||||
|
||||
def parse(self, line):
|
||||
|
|
@ -66,7 +67,7 @@ class _Socket(AbstractServer):
|
|||
args = line.split(' ')
|
||||
|
||||
if len(args):
|
||||
yield message.Command(cmd=args[0], args=args[1:], server=self._fd.fileno(), to=["you"], frm="you")
|
||||
yield message.Command(cmd=args[0], args=args[1:], server=self.fileno(), to=["you"], frm="you")
|
||||
|
||||
|
||||
def subparse(self, orig, cnt):
|
||||
|
|
@ -77,43 +78,50 @@ class _Socket(AbstractServer):
|
|||
yield m
|
||||
|
||||
|
||||
class SocketServer(_Socket):
|
||||
class _SocketServer(_Socket):
|
||||
|
||||
def __init__(self, host, port, bind=None, trynb=0, **kwargs):
|
||||
destlist = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
|
||||
(family, type, proto, canonname, self._sockaddr) = destlist[trynb%len(destlist)]
|
||||
def __init__(self, host, port, bind=None, **kwargs):
|
||||
(family, type, proto, canonname, sockaddr) = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)[0]
|
||||
|
||||
super().__init__(fdClass=socket.socket, family=family, type=type, proto=proto, **kwargs)
|
||||
super().__init__(family=family, type=type, proto=proto, **kwargs)
|
||||
|
||||
self._sockaddr = sockaddr
|
||||
self._bind = bind
|
||||
|
||||
|
||||
def connect(self):
|
||||
self._logger.info("Connecting to %s:%d", *self._sockaddr[:2])
|
||||
self.logger.info("Connection to %s:%d", *self._sockaddr[:2])
|
||||
super().connect(self._sockaddr)
|
||||
self._logger.info("Connected to %s:%d", *self._sockaddr[:2])
|
||||
|
||||
if self._bind:
|
||||
self._fd.bind(self._bind)
|
||||
super().bind(self._bind)
|
||||
|
||||
|
||||
class SocketServer(_SocketServer, socket.socket):
|
||||
pass
|
||||
|
||||
|
||||
class SecureSocketServer(_SocketServer, ssl.SSLSocket):
|
||||
pass
|
||||
|
||||
|
||||
class UnixSocket:
|
||||
|
||||
def __init__(self, location, **kwargs):
|
||||
super().__init__(fdClass=socket.socket, family=socket.AF_UNIX, **kwargs)
|
||||
super().__init__(family=socket.AF_UNIX, **kwargs)
|
||||
|
||||
self._socket_path = location
|
||||
|
||||
|
||||
def connect(self):
|
||||
self._logger.info("Connection to unix://%s", self._socket_path)
|
||||
self.connect(self._socket_path)
|
||||
self.logger.info("Connection to unix://%s", self._socket_path)
|
||||
super().connect(self._socket_path)
|
||||
|
||||
|
||||
class SocketClient(_Socket):
|
||||
class SocketClient(_Socket, socket.socket):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(fdClass=socket.socket, **kwargs)
|
||||
def read(self):
|
||||
return self.recv()
|
||||
|
||||
|
||||
class _Listener:
|
||||
|
|
@ -126,9 +134,9 @@ class _Listener:
|
|||
|
||||
|
||||
def read(self):
|
||||
conn, addr = self._fd.accept()
|
||||
conn, addr = self.accept()
|
||||
fileno = conn.fileno()
|
||||
self._logger.info("Accept new connection from %s (fd=%d)", addr, fileno)
|
||||
self.logger.info("Accept new connection from %s (fd=%d)", addr, fileno)
|
||||
|
||||
ss = self._instanciate(name=self.name + "#" + str(fileno), fileno=conn.detach())
|
||||
ss.connect = ss._on_connect
|
||||
|
|
@ -137,19 +145,23 @@ class _Listener:
|
|||
return b''
|
||||
|
||||
|
||||
class UnixSocketListener(_Listener, UnixSocket, _Socket):
|
||||
class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
def connect(self):
|
||||
self._logger.info("Creating Unix socket at unix://%s", self._socket_path)
|
||||
self.logger.info("Creating Unix socket at unix://%s", self._socket_path)
|
||||
|
||||
try:
|
||||
os.remove(self._socket_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
self._fd.bind(self._socket_path)
|
||||
self._fd.listen(5)
|
||||
self._logger.info("Socket ready for accepting new connections")
|
||||
self.bind(self._socket_path)
|
||||
self.listen(5)
|
||||
self.logger.info("Socket ready for accepting new connections")
|
||||
|
||||
self._on_connect()
|
||||
|
||||
|
|
@ -159,7 +171,7 @@ class UnixSocketListener(_Listener, UnixSocket, _Socket):
|
|||
import socket
|
||||
|
||||
try:
|
||||
self._fd.shutdown(socket.SHUT_RDWR)
|
||||
self.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
|
|
|
|||
50
nemubot/server/test_IRC.py
Normal file
50
nemubot/server/test_IRC.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import unittest
|
||||
|
||||
import nemubot.server.IRC as IRC
|
||||
|
||||
|
||||
class TestIRCMessage(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.msg = IRC.IRCMessage(b":toto!titi@RZ-3je16g.re PRIVMSG #the-channel :Can you parse this message?")
|
||||
|
||||
|
||||
def test_parsing(self):
|
||||
self.assertEqual(self.msg.prefix, "toto!titi@RZ-3je16g.re")
|
||||
self.assertEqual(self.msg.frm, "toto")
|
||||
self.assertEqual(self.msg.user, "titi")
|
||||
self.assertEqual(self.msg.host, "RZ-3je16g.re")
|
||||
|
||||
self.assertEqual(len(self.msg.params), 2)
|
||||
|
||||
self.assertEqual(self.msg.params[0], b"#the-channel")
|
||||
self.assertEqual(self.msg.params[1], b"Can you parse this message?")
|
||||
|
||||
|
||||
def test_prettyprint(self):
|
||||
bst1 = self.msg.to_server_string(False)
|
||||
msg2 = IRC.IRCMessage(bst1.encode())
|
||||
|
||||
bst2 = msg2.to_server_string(False)
|
||||
msg3 = IRC.IRCMessage(bst2.encode())
|
||||
|
||||
bst3 = msg3.to_server_string(False)
|
||||
|
||||
self.assertEqual(bst2, bst3)
|
||||
|
||||
|
||||
def test_tags(self):
|
||||
self.assertEqual(len(self.msg.tags), 1)
|
||||
self.assertIn("time", self.msg.tags)
|
||||
|
||||
self.msg.add_tag("time")
|
||||
self.assertEqual(len(self.msg.tags), 1)
|
||||
|
||||
self.msg.add_tag("toto")
|
||||
self.assertEqual(len(self.msg.tags), 2)
|
||||
self.assertIn("toto", self.msg.tags)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# 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/>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
|
||||
from nemubot.bot import sync_act
|
||||
|
||||
|
||||
class ThreadedServer:
|
||||
|
||||
"""A server backed by a library running in its own thread.
|
||||
|
||||
Uses an os.pipe() as a fake file descriptor to integrate with the bot's
|
||||
select.poll() main loop without requiring direct socket access.
|
||||
|
||||
When the library thread has a message ready, it calls _push_message(),
|
||||
which writes a wakeup byte to the pipe's write end. The bot's poll loop
|
||||
sees the read end become readable, calls async_read(), which drains the
|
||||
message queue and yields already-parsed bot-level messages.
|
||||
|
||||
This abstraction lets any IM library (IRC via python-irc, Matrix via
|
||||
matrix-nio, …) plug into nemubot without touching bot.py.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._logger = logging.getLogger("nemubot.server." + name)
|
||||
self._queue = queue.Queue()
|
||||
self._pipe_r, self._pipe_w = os.pipe()
|
||||
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def fileno(self):
|
||||
return self._pipe_r
|
||||
|
||||
|
||||
# Open/close
|
||||
|
||||
def connect(self):
|
||||
"""Start the library and register the pipe read-end with the poll loop."""
|
||||
self._logger.info("Starting connection")
|
||||
self._start()
|
||||
sync_act("sckt", "register", self._pipe_r)
|
||||
|
||||
def _start(self):
|
||||
"""Override: start the library's connection (e.g. launch a thread)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
"""Unregister from poll, stop the library, and close the pipe."""
|
||||
self._logger.info("Closing connection")
|
||||
sync_act("sckt", "unregister", self._pipe_r)
|
||||
self._stop()
|
||||
for fd in (self._pipe_w, self._pipe_r):
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _stop(self):
|
||||
"""Override: stop the library thread gracefully."""
|
||||
pass
|
||||
|
||||
|
||||
# Writes
|
||||
|
||||
def send_response(self, response):
|
||||
"""Override: send a response via the underlying library."""
|
||||
raise NotImplementedError
|
||||
|
||||
def async_write(self):
|
||||
"""No-op: writes go directly through the library, not via poll."""
|
||||
pass
|
||||
|
||||
|
||||
# Read
|
||||
|
||||
def _push_message(self, msg):
|
||||
"""Called from the library thread to enqueue a bot-level message.
|
||||
|
||||
Writes a wakeup byte to the pipe so the main loop wakes up and
|
||||
calls async_read().
|
||||
"""
|
||||
self._queue.put(msg)
|
||||
try:
|
||||
os.write(self._pipe_w, b'\x00')
|
||||
except OSError:
|
||||
pass # pipe closed during shutdown
|
||||
|
||||
def async_read(self):
|
||||
"""Called by the bot when the pipe is readable.
|
||||
|
||||
Drains the wakeup bytes and yields all queued bot messages.
|
||||
"""
|
||||
try:
|
||||
os.read(self._pipe_r, 256)
|
||||
except OSError:
|
||||
return
|
||||
while not self._queue.empty():
|
||||
try:
|
||||
yield self._queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
def parse(self, msg):
|
||||
"""Messages pushed via _push_message are already bot-level — pass through."""
|
||||
yield msg
|
||||
|
||||
|
||||
# Exceptions
|
||||
|
||||
def exception(self, flags):
|
||||
"""Called by the bot on POLLERR/POLLHUP/POLLNVAL."""
|
||||
self._logger.warning("Exception on server %s: flags=0x%x", self._name, flags)
|
||||
|
|
@ -82,16 +82,11 @@ class RSSEntry:
|
|||
else:
|
||||
self.summary = None
|
||||
|
||||
if len(node.getElementsByTagName("link")) > 0:
|
||||
self.link = node.getElementsByTagName("link")[0].firstChild.nodeValue
|
||||
if len(node.getElementsByTagName("link")) > 0 and node.getElementsByTagName("link")[0].hasAttribute("href"):
|
||||
self.link = node.getElementsByTagName("link")[0].getAttribute("href")
|
||||
else:
|
||||
self.link = None
|
||||
|
||||
if len(node.getElementsByTagName("enclosure")) > 0 and node.getElementsByTagName("enclosure")[0].hasAttribute("url"):
|
||||
self.enclosure = node.getElementsByTagName("enclosure")[0].getAttribute("url")
|
||||
else:
|
||||
self.enclosure = None
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "<RSSEntry title='%s' updated='%s'>" % (self.title, self.pubDate)
|
||||
|
|
@ -110,13 +105,13 @@ class Feed:
|
|||
self.updated = None
|
||||
self.entries = list()
|
||||
|
||||
if self.feed.tagName == "rdf:RDF" or self.feed.tagName == "rss":
|
||||
if self.feed.tagName == "rss":
|
||||
self._parse_rss_feed()
|
||||
elif self.feed.tagName == "feed":
|
||||
self._parse_atom_feed()
|
||||
else:
|
||||
from nemubot.exception import IMException
|
||||
raise IMException("This is not a valid Atom or RSS feed.")
|
||||
raise IMException("This is not a valid Atom or RSS feed")
|
||||
|
||||
|
||||
def _parse_atom_feed(self):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from nemubot.exception import IMException
|
|||
def isURL(url):
|
||||
"""Return True if the URL can be parsed"""
|
||||
o = urlparse(_getNormalizedURL(url))
|
||||
return o.netloc != "" and o.path != ""
|
||||
return o.netloc == "" and o.path == ""
|
||||
|
||||
|
||||
def _getNormalizedURL(url):
|
||||
|
|
@ -68,7 +68,18 @@ def getPassword(url):
|
|||
|
||||
# Get real pages
|
||||
|
||||
def _URLConn(cb, url, body=None, timeout=7, header=None, follow_redir=True):
|
||||
def getURLContent(url, body=None, timeout=7, header=None, decode_error=False,
|
||||
max_size=524288):
|
||||
"""Return page content corresponding to URL or None if any error occurs
|
||||
|
||||
Arguments:
|
||||
url -- the URL to get
|
||||
body -- Data to send as POST content
|
||||
timeout -- maximum number of seconds to wait before returning an exception
|
||||
decode_error -- raise exception on non-200 pages or ignore it
|
||||
max_size -- maximal size allow for the content
|
||||
"""
|
||||
|
||||
o = urlparse(_getNormalizedURL(url), "http")
|
||||
|
||||
import http.client
|
||||
|
|
@ -123,53 +134,10 @@ def _URLConn(cb, url, body=None, timeout=7, header=None, follow_redir=True):
|
|||
|
||||
try:
|
||||
res = conn.getresponse()
|
||||
if follow_redir and ((res.status == http.client.FOUND or
|
||||
res.status == http.client.MOVED_PERMANENTLY) and
|
||||
res.getheader("Location") != url):
|
||||
return _URLConn(cb,
|
||||
url=urljoin(url, res.getheader("Location")),
|
||||
body=body,
|
||||
timeout=timeout,
|
||||
header=header,
|
||||
follow_redir=follow_redir)
|
||||
return cb(res)
|
||||
except http.client.BadStatusLine:
|
||||
raise IMException("Invalid HTTP response")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def getURLHeaders(url, body=None, timeout=7, header=None, follow_redir=True):
|
||||
"""Return page headers corresponding to URL or None if any error occurs
|
||||
|
||||
Arguments:
|
||||
url -- the URL to get
|
||||
body -- Data to send as POST content
|
||||
timeout -- maximum number of seconds to wait before returning an exception
|
||||
"""
|
||||
|
||||
def next(res):
|
||||
return res.status, res.getheaders()
|
||||
return _URLConn(next, url=url, body=body, timeout=timeout, header=header, follow_redir=follow_redir)
|
||||
|
||||
|
||||
def getURLContent(url, body=None, timeout=7, header=None, decode_error=False,
|
||||
max_size=524288):
|
||||
"""Return page content corresponding to URL or None if any error occurs
|
||||
|
||||
Arguments:
|
||||
url -- the URL to get
|
||||
body -- Data to send as POST content
|
||||
timeout -- maximum number of seconds to wait before returning an exception
|
||||
decode_error -- raise exception on non-200 pages or ignore it
|
||||
max_size -- maximal size allow for the content
|
||||
"""
|
||||
|
||||
def _nextURLContent(res):
|
||||
size = int(res.getheader("Content-Length", 524288))
|
||||
cntype = res.getheader("Content-Type")
|
||||
|
||||
if max_size >= 0 and (size > max_size or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl")):
|
||||
if size > max_size or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"):
|
||||
raise IMException("Content too large to be retrieved")
|
||||
|
||||
data = res.read(size)
|
||||
|
|
@ -187,18 +155,28 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False,
|
|||
charset = cha[1]
|
||||
else:
|
||||
charset = cha[0]
|
||||
except http.client.BadStatusLine:
|
||||
raise IMException("Invalid HTTP response")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
import http.client
|
||||
|
||||
if res.status == http.client.OK or res.status == http.client.SEE_OTHER:
|
||||
return data.decode(charset, errors='ignore').strip()
|
||||
elif decode_error:
|
||||
return data.decode(charset, errors='ignore').strip()
|
||||
else:
|
||||
raise IMException("A HTTP error occurs: %d - %s" %
|
||||
(res.status, http.client.responses[res.status]))
|
||||
|
||||
return _URLConn(_nextURLContent, url=url, body=body, timeout=timeout, header=header)
|
||||
if res.status == http.client.OK or res.status == http.client.SEE_OTHER:
|
||||
return data.decode(charset).strip()
|
||||
elif ((res.status == http.client.FOUND or
|
||||
res.status == http.client.MOVED_PERMANENTLY) and
|
||||
res.getheader("Location") != url):
|
||||
return getURLContent(
|
||||
urljoin(url, res.getheader("Location")),
|
||||
body=body,
|
||||
timeout=timeout,
|
||||
header=header,
|
||||
decode_error=decode_error,
|
||||
max_size=max_size)
|
||||
elif decode_error:
|
||||
return data.decode(charset).strip()
|
||||
else:
|
||||
raise IMException("A HTTP error occurs: %d - %s" %
|
||||
(res.status, http.client.responses[res.status]))
|
||||
|
||||
|
||||
def getXML(*args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class XMLParser:
|
|||
@property
|
||||
def root(self):
|
||||
if len(self.stack):
|
||||
return self.stack[0][0]
|
||||
return self.stack[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
@ -91,13 +91,13 @@ class XMLParser:
|
|||
@property
|
||||
def current(self):
|
||||
if len(self.stack):
|
||||
return self.stack[-1][0]
|
||||
return self.stack[-1]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def display_stack(self):
|
||||
return " in ".join([str(type(s).__name__) for s,c in reversed(self.stack)])
|
||||
return " in ".join([str(type(s).__name__) for s in reversed(self.stack)])
|
||||
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
|
|
@ -105,8 +105,7 @@ class XMLParser:
|
|||
if name not in self.knodes:
|
||||
raise TypeError(name + " is not a known type to decode")
|
||||
else:
|
||||
self.stack.append((self.knodes[name](**attrs), self.child))
|
||||
self.child = 0
|
||||
self.stack.append(self.knodes[name](**attrs))
|
||||
else:
|
||||
self.child += 1
|
||||
|
||||
|
|
@ -117,15 +116,19 @@ class XMLParser:
|
|||
|
||||
|
||||
def endElement(self, name):
|
||||
if hasattr(self.current, "endElement"):
|
||||
self.current.endElement(None)
|
||||
|
||||
if self.child:
|
||||
self.child -= 1
|
||||
|
||||
if hasattr(self.current, "endElement"):
|
||||
self.current.endElement(name)
|
||||
return
|
||||
|
||||
if hasattr(self.current, "endElement"):
|
||||
self.current.endElement(None)
|
||||
|
||||
# Don't remove root
|
||||
elif len(self.stack) > 1:
|
||||
last, self.child = self.stack.pop()
|
||||
if len(self.stack) > 1:
|
||||
last = self.stack.pop()
|
||||
if hasattr(self.current, "addChild"):
|
||||
if self.current.addChild(name, last):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import types
|
||||
|
||||
logger = logging.getLogger("nemubot.treatment")
|
||||
|
||||
|
|
@ -78,12 +79,19 @@ class MessageTreater:
|
|||
|
||||
for h in self.hm.get_hooks("pre", type(msg).__name__):
|
||||
if h.can_read(msg.to, msg.server) and h.match(msg):
|
||||
for res in flatify(h.run(msg)):
|
||||
if res is not None and res != msg:
|
||||
yield from self._pre_treat(res)
|
||||
res = h.run(msg)
|
||||
|
||||
elif res is None or res is False:
|
||||
break
|
||||
if isinstance(res, list):
|
||||
for i in range(len(res)):
|
||||
# Avoid infinite loop
|
||||
if res[i] != msg:
|
||||
yield from self._pre_treat(res[i])
|
||||
|
||||
elif res is not None and res != msg:
|
||||
yield from self._pre_treat(res)
|
||||
|
||||
elif res is None or res is False:
|
||||
break
|
||||
else:
|
||||
yield msg
|
||||
|
||||
|
|
@ -105,10 +113,25 @@ class MessageTreater:
|
|||
msg.frm_owner = (not hasattr(msg.server, "owner") or msg.server.owner == msg.frm)
|
||||
|
||||
while hook is not None:
|
||||
for res in flatify(hook.run(msg)):
|
||||
if not hasattr(res, "server") or res.server is None:
|
||||
res.server = msg.server
|
||||
yield res
|
||||
res = hook.run(msg)
|
||||
|
||||
if isinstance(res, list):
|
||||
for r in res:
|
||||
yield r
|
||||
|
||||
elif res is not None:
|
||||
if isinstance(res, types.GeneratorType):
|
||||
for r in res:
|
||||
if not hasattr(r, "server") or r.server is None:
|
||||
r.server = msg.server
|
||||
|
||||
yield r
|
||||
|
||||
else:
|
||||
if not hasattr(res, "server") or res.server is None:
|
||||
res.server = msg.server
|
||||
|
||||
yield res
|
||||
|
||||
hook = next(hook_gen, None)
|
||||
|
||||
|
|
@ -142,20 +165,19 @@ class MessageTreater:
|
|||
|
||||
for h in self.hm.get_hooks("post", type(msg).__name__):
|
||||
if h.can_write(msg.to, msg.server) and h.match(msg):
|
||||
for res in flatify(h.run(msg)):
|
||||
if res is not None and res != msg:
|
||||
yield from self._post_treat(res)
|
||||
res = h.run(msg)
|
||||
|
||||
elif res is None or res is False:
|
||||
break
|
||||
if isinstance(res, list):
|
||||
for i in range(len(res)):
|
||||
# Avoid infinite loop
|
||||
if res[i] != msg:
|
||||
yield from self._post_treat(res[i])
|
||||
|
||||
elif res is not None and res != msg:
|
||||
yield from self._post_treat(res)
|
||||
|
||||
elif res is None or res is False:
|
||||
break
|
||||
|
||||
else:
|
||||
yield msg
|
||||
|
||||
|
||||
def flatify(g):
|
||||
if hasattr(g, "__iter__"):
|
||||
for i in g:
|
||||
yield from flatify(i)
|
||||
else:
|
||||
yield g
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
irc
|
||||
matrix-nio
|
||||
2
setup.py
2
setup.py
|
|
@ -69,8 +69,8 @@ setup(
|
|||
'nemubot.hooks.keywords',
|
||||
'nemubot.message',
|
||||
'nemubot.message.printer',
|
||||
'nemubot.module',
|
||||
'nemubot.server',
|
||||
'nemubot.server.message',
|
||||
'nemubot.tools',
|
||||
'nemubot.tools.xmlparser',
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue