aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Cache.py8
-rw-r--r--src/Control.py7
-rw-r--r--src/IRCBot.py11
-rw-r--r--src/IRCUser.py11
-rw-r--r--src/LockFile.py6
-rw-r--r--src/ModuleManager.py10
-rw-r--r--src/utils/datetime.py4
-rw-r--r--src/utils/http.py136
-rw-r--r--src/utils/irc.py24
-rw-r--r--src/utils/parse.py11
10 files changed, 112 insertions, 116 deletions
diff --git a/src/Cache.py b/src/Cache.py
index f28a0952..97e09268 100644
--- a/src/Cache.py
+++ b/src/Cache.py
@@ -14,7 +14,7 @@ class Cache(PollHook.PollHook):
return self._cache(key, value, None)
def temporary_cache(self, key: str, value: typing.Any, timeout: float
)-> str:
- return self._cache(key, value, time.monotonic()+timeout)
+ return self._cache(key, value, time.time()+timeout)
def _cache(self, key: str, value: typing.Any,
expiration: typing.Optional[float]) -> str:
id = self.cache_key(key)
@@ -30,7 +30,7 @@ class Cache(PollHook.PollHook):
expirations = list(filter(None, expirations))
if not expirations:
return None
- now = time.monotonic()
+ now = time.time()
expirations = [e-now for e in expirations]
expiration = max(min(expirations), 0)
@@ -38,7 +38,7 @@ class Cache(PollHook.PollHook):
return expiration
def call(self):
- now = time.monotonic()
+ now = time.time()
expired = []
for id in self._items.keys():
key, value, expiration = self._items[id]
@@ -63,4 +63,4 @@ class Cache(PollHook.PollHook):
return expiration
def until_expiration(self, key: typing.Any) -> float:
expiration = self.get_expiration(key)
- return expiration-time.monotonic()
+ return expiration-time.time()
diff --git a/src/Control.py b/src/Control.py
index 6f864303..ed5a5351 100644
--- a/src/Control.py
+++ b/src/Control.py
@@ -104,6 +104,13 @@ class Control(PollSource.PollSource):
keepalive = False
elif command == "stop":
self._bot.stop()
+ elif command == "command" and data:
+ subcommand, _, data = data.partition(" ")
+ output = self._bot._events.on("control").on(subcommand
+ ).call_for_result(data=data)
+ if not output == None:
+ response_data = output
+ keepalive = False
self._send_action(client, response_action, response_data, id)
if not keepalive:
diff --git a/src/IRCBot.py b/src/IRCBot.py
index bd00b0d6..9d354245 100644
--- a/src/IRCBot.py
+++ b/src/IRCBot.py
@@ -1,13 +1,14 @@
+VERSION: str = ""
+with open("VERSION", "r") as version_file:
+ VERSION = "v%s" % version_file.read().strip()
+SOURCE: str = "https://git.io/bitbot"
+URL: str = "https://bitbot.dev"
+
import enum, queue, os, queue, select, socket, sys, threading, time, traceback
import typing, uuid
from src import EventManager, Exports, IRCServer, Logging, ModuleManager
from src import PollHook, PollSource, Socket, Timers, utils
-with open("VERSION", "r") as version_file:
- VERSION = "v%s" % version_file.read().strip()
-SOURCE = "https://git.io/bitbot"
-URL = "https://bitbot.dev"
-
class TriggerResult(enum.Enum):
Return = 1
Exception = 2
diff --git a/src/IRCUser.py b/src/IRCUser.py
index 5195cfc0..2e141794 100644
--- a/src/IRCUser.py
+++ b/src/IRCUser.py
@@ -11,17 +11,15 @@ class User(IRCObject.Object):
self.server = server
self.set_nickname(nickname)
self._id = id
+ self._id_override: typing.Optional[int] = None
self.username: typing.Optional[str] = None
self.hostname: typing.Optional[str] = None
self.realname: typing.Optional[str] = None
self.bot = bot
self.channels: typing.Set[IRCChannel.Channel] = set([])
- self.identified_account = None
- self.identified_account_override = None
+ self.account = None
- self.identified_account_id = None
- self.identified_account_id_override = None
self.away = False
self.away_message: typing.Optional[str] = None
@@ -42,10 +40,7 @@ class User(IRCObject.Object):
return None
def get_id(self)-> int:
- return (self.identified_account_id_override or
- self.identified_account_id or self._id)
- def get_identified_account(self) -> typing.Optional[str]:
- return (self.identified_account_override or self.identified_account)
+ return self._id_override or self._id
def set_nickname(self, nickname: str):
self.nickname = nickname
diff --git a/src/LockFile.py b/src/LockFile.py
index 309e8e87..54ff6284 100644
--- a/src/LockFile.py
+++ b/src/LockFile.py
@@ -9,7 +9,7 @@ class LockFile(PollHook.PollHook):
self._next_lock = None
def available(self):
- now = utils.datetime.datetime_utcnow()
+ now = utils.datetime.utcnow()
if os.path.exists(self._filename):
with open(self._filename, "r") as lock_file:
timestamp_str = lock_file.read().strip().split(" ", 1)[0]
@@ -23,14 +23,14 @@ class LockFile(PollHook.PollHook):
def lock(self):
with open(self._filename, "w") as lock_file:
- last_lock = utils.datetime.datetime_utcnow()
+ last_lock = utils.datetime.utcnow()
lock_file.write("%s" % utils.datetime.iso8601_format(last_lock))
self._next_lock = last_lock+datetime.timedelta(
seconds=EXPIRATION/2)
def next(self):
return max(0,
- (self._next_lock-utils.datetime.datetime_utcnow()).total_seconds())
+ (self._next_lock-utils.datetime.utcnow()).total_seconds())
def call(self):
self.lock()
diff --git a/src/ModuleManager.py b/src/ModuleManager.py
index 50af3493..5ef57b73 100644
--- a/src/ModuleManager.py
+++ b/src/ModuleManager.py
@@ -88,10 +88,12 @@ class ModuleDefinition(object):
class LoadedModule(object):
def __init__(self,
name: str,
+ title: str,
module: BaseModule,
context: str,
import_name: str):
self.name = name
+ self.title = title
self.module = module
self.context = context
self.import_name = import_name
@@ -233,8 +235,8 @@ class ModuleManager(object):
module_object = module_object_pointer(bot, context_events,
context_exports, context_timers, self.log)
- if not hasattr(module_object, "_name"):
- module_object._name = definition.name.title()
+ module_title = (getattr(module_object, "_name", None) or
+ definition.name.title())
# @utils.hook() magic
for attribute_name in dir(module_object):
@@ -256,8 +258,8 @@ class ModuleManager(object):
raise ModuleNameCollisionException("Module name '%s' "
"attempted to be used twice" % definition.name)
- return LoadedModule(definition.name, module_object, context,
- import_name)
+ return LoadedModule(definition.name, module_title, module_object,
+ context, import_name)
def load_module(self, bot: "IRCBot.Bot", definition: ModuleDefinition
) -> LoadedModule:
diff --git a/src/utils/datetime.py b/src/utils/datetime.py
index 0fac2bb3..3ed03088 100644
--- a/src/utils/datetime.py
+++ b/src/utils/datetime.py
@@ -10,7 +10,7 @@ ISO8601_FORMAT_TZ = "%z"
DATETIME_HUMAN = "%Y/%m/%d %H:%M:%S"
DATE_HUMAN = "%Y-%m-%d"
-def datetime_utcnow() -> _datetime.datetime:
+def utcnow() -> _datetime.datetime:
return _datetime.datetime.utcnow().replace(tzinfo=_datetime.timezone.utc)
def datetime_timestamp(seconds: float) -> _datetime.datetime:
return _datetime.datetime.fromtimestamp(seconds).replace(
@@ -26,7 +26,7 @@ def iso8601_format(dt: _datetime.datetime, milliseconds: bool=False) -> str:
return "%s%s%s" % (dt_format, ms_format, tz_format)
def iso8601_format_now(milliseconds: bool=False) -> str:
- return iso8601_format(datetime_utcnow(), milliseconds=milliseconds)
+ return iso8601_format(utcnow(), milliseconds=milliseconds)
def iso8601_parse(s: str, microseconds: bool=False) -> _datetime.datetime:
fmt = ISO8601_PARSE_MICROSECONDS if microseconds else ISO8601_PARSE
return _datetime.datetime.strptime(s, fmt)
diff --git a/src/utils/http.py b/src/utils/http.py
index f31da62c..699c48f1 100644
--- a/src/utils/http.py
+++ b/src/utils/http.py
@@ -1,9 +1,8 @@
-import asyncio, codecs, ipaddress, re, signal, socket, traceback, typing
-import urllib.error, urllib.parse, uuid
+import asyncio, codecs, dataclasses, ipaddress, re, signal, socket, traceback
+import typing, urllib.error, urllib.parse, uuid
import json as _json
-import bs4, netifaces, requests
-import tornado.httpclient
-from src import utils
+import bs4, netifaces, requests, tornado.httpclient
+from src import IRCBot, utils
REGEX_URL = re.compile("https?://\S+", re.I)
@@ -29,8 +28,8 @@ def url_sanitise(url: str):
url = url[:-1]
return url
-DEFAULT_USERAGENT = ("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 "
- "(KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36")
+USERAGENT = "Mozilla/5.0 (compatible; BitBot/%s; +%s" % (
+ IRCBot.VERSION, IRCBot.URL)
RESPONSE_MAX = (1024*1024)*100
SOUP_CONTENT_TYPES = ["text/html", "text/xml", "application/xml"]
@@ -54,46 +53,33 @@ class HTTPWrongContentTypeException(HTTPException):
def throw_timeout():
raise HTTPTimeoutException()
+@dataclasses.dataclass
class Request(object):
- def __init__(self, url: str,
- get_params: typing.Dict[str, str]={}, post_data: typing.Any=None,
- headers: typing.Dict[str, str]={},
+ url: str
+ id: typing.Optional[str] = None
+ method: str = "GET"
- json: bool=False, json_body: bool=False, allow_redirects: bool=True,
- check_content_type: bool=True, parse: bool=False,
- detect_encoding: bool=True,
+ get_params: typing.Dict[str, str] = dataclasses.field(
+ default_factory=dict)
+ post_data: typing.Any = None
+ headers: typing.Dict[str, str] = dataclasses.field(
+ default_factory=dict)
+ cookies: typing.Dict[str, str] = dataclasses.field(
+ default_factory=dict)
- method: str="GET", parser: str="lxml", id: str=None,
- fallback_encoding: str=None, content_type: str=None,
- proxy: str=None, useragent: str=None,
+ json_body: bool = False
- **kwargs):
- self.id = id or str(uuid.uuid4())
+ allow_redirects: bool = True
+ check_content_type: bool = True
+ fallback_encoding: typing.Optional[str] = None
+ content_type: typing.Optional[str] = None
+ proxy: typing.Optional[str] = None
+ useragent: typing.Optional[str] = None
- self.set_url(url)
- self.method = method.upper()
- self.get_params = get_params
- self.post_data = post_data
- self.headers = headers
-
- self.json = json
- self.json_body = json_body
- self.allow_redirects = allow_redirects
- self.check_content_type = check_content_type
- self.parse = parse
- self.detect_encoding = detect_encoding
-
- self.parser = parser
- self.fallback_encoding = fallback_encoding
- self.content_type = content_type
- self.proxy = proxy
- self.useragent = useragent
-
- if kwargs:
- if method == "POST":
- self.post_data = kwargs
- else:
- self.get_params.update(kwargs)
+ def validate(self):
+ self.id = self.id or str(uuid.uuid4())
+ self.set_url(self.url)
+ self.method = self.method.upper()
def set_url(self, url: str):
parts = urllib.parse.urlparse(url)
@@ -113,7 +99,7 @@ class Request(object):
if not "Accept-Language" in headers:
headers["Accept-Language"] = "en-GB"
if not "User-Agent" in headers:
- headers["User-Agent"] = self.useragent or DEFAULT_USERAGENT
+ headers["User-Agent"] = self.useragent or USERAGENT
if not "Content-Type" in headers and self.content_type:
headers["Content-Type"] = self.content_type
return headers
@@ -128,13 +114,20 @@ class Request(object):
return None
class Response(object):
- def __init__(self, code: int, data: typing.Any,
- headers: typing.Dict[str, str], encoding: str):
+ def __init__(self, code: int, data: bytes, encoding: str,
+ headers: typing.Dict[str, str], cookies: typing.Dict[str, str]):
self.code = code
self.data = data
- self.headers = headers
self.content_type = headers.get("Content-Type", "").split(";", 1)[0]
self.encoding = encoding
+ self.headers = headers
+ self.cookies = cookies
+ def decode(self, encoding="utf8") -> str:
+ return self.data.decode(encoding)
+ def json(self) -> typing.Any:
+ return _json.loads(self.data)
+ def soup(self, parser: str="lxml") -> bs4.BeautifulSoup:
+ return bs4.BeautifulSoup(self.decode(), parser)
def _meta_content(s: str) -> typing.Dict[str, str]:
out = {}
@@ -143,7 +136,8 @@ def _meta_content(s: str) -> typing.Dict[str, str]:
out[key] = value
return out
-def _find_encoding(soup: bs4.BeautifulSoup) -> typing.Optional[str]:
+def _find_encoding(data: bytes) -> typing.Optional[str]:
+ soup = bs4.BeautifulSoup(data, "lxml")
if not soup.meta == None:
meta_charset = soup.meta.get("charset")
if not meta_charset == None:
@@ -167,7 +161,7 @@ def request(request_obj: typing.Union[str, Request], **kwargs) -> Response:
return _request(request_obj)
def _request(request_obj: Request) -> Response:
-
+ request_obj.validate()
def _wrap() -> Response:
headers = request_obj.get_headers()
response = requests.request(
@@ -177,7 +171,8 @@ def _request(request_obj: Request) -> Response:
params=request_obj.get_params,
data=request_obj.get_body(),
allow_redirects=request_obj.allow_redirects,
- stream=True
+ stream=True,
+ cookies=request_obj.cookies
)
response_content = response.raw.read(RESPONSE_MAX,
decode_content=True)
@@ -186,7 +181,8 @@ def _request(request_obj: Request) -> Response:
headers = utils.CaseInsensitiveDict(dict(response.headers))
our_response = Response(response.status_code, response_content,
- headers=headers, encoding=response.encoding)
+ encoding=response.encoding, headers=headers,
+ cookies=response.cookies.get_dict())
return our_response
try:
@@ -202,39 +198,12 @@ def _request(request_obj: Request) -> Response:
else:
encoding = "iso-8859-1"
- if (request_obj.detect_encoding and
- response.content_type and
+ if (response.content_type and
response.content_type in SOUP_CONTENT_TYPES):
- souped = bs4.BeautifulSoup(response.data, request_obj.parser)
- encoding = _find_encoding(souped) or encoding
-
- def _decode_data():
- return response.data.decode(encoding)
-
- if request_obj.parse:
- if (not request_obj.check_content_type or
- response.content_type in SOUP_CONTENT_TYPES):
- souped = bs4.BeautifulSoup(_decode_data(), request_obj.parser)
- response.data = souped
- return response
- else:
- raise HTTPWrongContentTypeException(
- "Tried to soup non-html/non-xml data (%s)" %
- response.content_type)
+ encoding = _find_encoding(response.data) or encoding
+ response.encoding = encoding
- if request_obj.json and response.data:
- data = _decode_data()
- try:
- response.data = _json.loads(data)
- return response
- except _json.decoder.JSONDecodeError as e:
- raise HTTPParsingException(str(e), data)
-
- if response.content_type in DECODE_CONTENT_TYPES:
- response.data = _decode_data()
- return response
- else:
- return response
+ return response
class RequestManyException(Exception):
pass
@@ -242,6 +211,7 @@ def request_many(requests: typing.List[Request]) -> typing.Dict[str, Response]:
responses = {}
async def _request(request):
+ request.validate()
client = tornado.httpclient.AsyncHTTPClient()
url = request.url
if request.get_params:
@@ -263,8 +233,8 @@ def request_many(requests: typing.List[Request]) -> typing.Dict[str, Response]:
"request_many failed for %s" % url)
headers = utils.CaseInsensitiveDict(dict(response.headers))
- data = response.body.decode("utf8")
- responses[request.id] = Response(response.code, data, headers, "utf8")
+ responses[request.id] = Response(response.code, response.body, "utf8",
+ headers, {})
loop = asyncio.new_event_loop()
awaits = []
diff --git a/src/utils/irc.py b/src/utils/irc.py
index 30a3126e..cdaa61eb 100644
--- a/src/utils/irc.py
+++ b/src/utils/irc.py
@@ -38,19 +38,29 @@ def color(s: str, foreground: consts.IRCColor,
if background:
background_s = ",%s" % str(background.irc).zfill(2)
- return "%s%s%s%s%s" % (consts.COLOR, foreground_s, background_s, s,
- consts.COLOR)
+ return f"{consts.COLOR}{foreground_s}{background_s}{s}{consts.COLOR}"
-HASH_COLORS = list(range(2, 16))
+HASH_STOP = ["_", "|", "["]
+HASH_COLORS = [consts.CYAN, consts.PURPLE, consts.GREEN, consts.ORANGE,
+ consts.LIGHTBLUE, consts.TRANSPARENT, consts.LIGHTCYAN, consts.PINK,
+ consts.LIGHTGREEN, consts.BLUE]
def hash_colorize(s: str):
- hash_code = sum(ord(c) for c in s.lower())%len(HASH_COLORS)
- return color(s, consts.COLOR_CODES[HASH_COLORS[hash_code]])
+ hash = 5381
+ non_stop = False
+ for i, char in enumerate(s):
+ if not char in HASH_STOP:
+ non_stop = True
+ elif non_stop:
+ break
+ hash ^= ((hash<<5)+(hash>>2)+ord(char))&0xFFFFFFFFFFFFFFFF
+
+ return color(s, HASH_COLORS[hash%len(HASH_COLORS)])
def bold(s: str) -> str:
- return "%s%s%s" % (consts.BOLD, s, consts.BOLD)
+ return f"{consts.BOLD}{s}{consts.BOLD}"
def underline(s: str) -> str:
- return "%s%s%s" % (consts.UNDERLINE, s, consts.UNDERLINE)
+ return f"{consts.UNDERLINE}{s}{consts.UNDERLINE}"
def strip_font(s: str) -> str:
s = s.replace(consts.BOLD, "")
diff --git a/src/utils/parse.py b/src/utils/parse.py
index d5018441..ce2ee793 100644
--- a/src/utils/parse.py
+++ b/src/utils/parse.py
@@ -1,4 +1,5 @@
import decimal, io, typing
+from . import datetime, errors
COMMENT_TYPES = ["#", "//"]
def hashflags(filename: str
@@ -109,3 +110,13 @@ def parse_number(s: str) -> str:
raise ValueError("Unknown unit '%s' given to parse_number" % unit)
return str(number)
+def timed_args(args, min_args):
+ if args and args[0][0] == "+":
+ if len(args[1:]) < min_args:
+ raise errors.EventError("Not enough arguments")
+ time = datetime.from_pretty_time(args[0][1:])
+ if time == None:
+ raise errors.EventError("Invalid timeframe")
+ return time, args[1:]
+ return None, args
+