diff --git a/legacy/.env.example b/legacy/.env.example new file mode 100644 index 0000000..cdc5bd8 --- /dev/null +++ b/legacy/.env.example @@ -0,0 +1,13 @@ +THRESHOLD_LISTENER_HOST=0.0.0.0 +THRESHOLD_LISTENER_PORT=13867 +THRESHOLD_LISTENER_SSL=1 + +THRESHOLD_RELAY_ENABLED=1 +THRESHOLD_RELAY_HOST=0.0.0.0 +THRESHOLD_RELAY_PORT=13868 +THRESHOLD_RELAY_SSL=1 + +THRESHOLD_API_ENABLED=1 +THRESHOLD_API_HOST=0.0.0.0 +THRESHOLD_API_PORT=13869 +PORTAINER_GIT_DIR=. \ No newline at end of file diff --git a/legacy/.gitignore b/legacy/.gitignore new file mode 100644 index 0000000..0396584 --- /dev/null +++ b/legacy/.gitignore @@ -0,0 +1,11 @@ +*.pyc +*.pem +*.swp +__pycache__/ +conf/live/ +conf/cert/ +env/ +venv/ +.idea/ +.env +.bash_history diff --git a/legacy/.pre-commit-config.yaml b/legacy/.pre-commit-config.yaml new file mode 100644 index 0000000..673c866 --- /dev/null +++ b/legacy/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black + args: + - --line-length=120 + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + args: ["--profile", "black"] + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + args: [--max-line-length=120] diff --git a/legacy/api/views.py b/legacy/api/views.py new file mode 100644 index 0000000..443e546 --- /dev/null +++ b/legacy/api/views.py @@ -0,0 +1,699 @@ +import functools +from json import JSONDecodeError, dumps, loads +from string import digits + +from klein import Klein +from twisted.web.server import Request + +import main +from modules import chankeep, helpers, provision, regproc, userinfo +from modules.network import Network +from utils.logging.log import warn + + +def login_required(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if isinstance(args[0], Request): + request = args[0] + apikey = request.getHeader("ApiKey") + token = request.getHeader("Token") + if not apikey: + return "No API key provided" + if not token: + return "No token provided" + if apikey not in main.tokens: + return "No such API key" + config_token = main.tokens[apikey] + if not token == config_token["hello"]: + return "Invalid token" + counter = config_token["counter"] + request.setHeader("Counter", counter) + return func(self, *args, **kwargs) + + return wrapper + + +class API(object): + """ + Our API webapp. + """ + + app = Klein() + + @app.route("/", methods=["GET"]) + @login_required + def hello(self, request): + return "Hello" + + @app.route("/who//", methods=["POST"]) + @login_required + def who(self, request, query): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "query" not in data: + return "No query provided" + result = userinfo.getWho(data["query"]) + # Expand the generator + return dumps({k: [x for x in v] for k, v in result.items()}) + + @app.route("/chans/", methods=["POST"]) + @login_required + def chans(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "net" not in data: + return "No net provided" + if "query" not in data: + return "No query provided" + if not data["query"]: + warn(f"No query provided: for chans {data}") + return dumps({}) + result = userinfo.getChansSingle(data["net"], data["query"]) + if not result: + return dumps({}) + return dumps({"chans": [x for x in result]}) + + @app.route("/users/", methods=["POST"]) + @login_required + def users(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "net" not in data: + return "No net provided" + if "query" not in data: + return "No query provided" + if not data["query"]: + warn(f"No query provided for users: {data}") + return dumps({}) + result = userinfo.getUsersSingle(data["net"], data["query"]) + if not result: + return dumps({}) + return dumps({"users": [x for x in result]}) + + @app.route("/online/", methods=["POST"]) + @login_required + def online(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "net" not in data: + return "No net provided" + if "query" not in data: + return "No users provided" + if not data["query"]: + warn(f"No users provided: for online {data}") + return dumps({}) + net = data["net"] + usermap = {} + for user in data["query"]: + channels = userinfo.getChansSingle(net, [user]) + if channels: + usermap[user] = True + else: + usermap[user] = False + + return dumps(usermap) + + @app.route("/num_users/", methods=["POST"]) + @login_required + def num_users(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "net" not in data: + return "No net provided" + if "query" not in data: + return "No users provided" + if not data["query"]: + warn(f"No chans provided: for num_users {data}") + return dumps({}) + net = data["net"] + results = userinfo.getUserNum(net, data["query"]) + + return dumps(results) + + @app.route("/num_chans/", methods=["POST"]) + @login_required + def num_chans(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "net" not in data: + return "No net provided" + if "query" not in data: + return "No users provided" + if not data["query"]: + warn(f"No users provided: for num_chans {data}") + return dumps({}) + net = data["net"] + results = userinfo.getChanNum(net, data["query"]) + + return dumps(results) + + @app.route("/irc/stats/", methods=["POST"]) + @login_required + def irc_stats(self, request): + stats = {} + numChannels = 0 + numWhoEntries = 0 + for i in main.IRCPool.keys(): + numChannels += len(main.IRCPool[i].channels) + numWhoEntries += userinfo.getNumTotalWhoEntries() + numRelays = 0 + for net in main.network.keys(): + numRelays += len(main.network[net].relays) + stats["servers_total_total"] = numRelays + stats["servers_total_unique"] = len(main.network.keys()) + stats["servers_online_total"] = len(main.IRCPool.keys()) + stats["servers_online_unique"] = len(main.liveNets()) + stats["channels"] = numChannels + stats["records"] = numWhoEntries + stats["eventrate"] = main.lastMinuteSample + return dumps(stats) + + @app.route("/irc/networks/", methods=["POST"]) + @login_required + def irc_networks(self, request): + networks = {} + for net in main.network.keys(): + networks[net] = { + "active": chankeep.allRelaysActive(net), + "relays": len(main.network[net].relays), + "channels": userinfo.getTotalChanNum(net), + "records": userinfo.getNumWhoEntries(net), + } + return dumps(networks) + + @app.route("/irc/auth/", methods=["POST"]) + @login_required + def irc_network_recheckauth(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "func" not in data: + return dumps({"success": False, "reason": "no function specified."}) + func = data["func"] + + if "net" not in data: + return dumps({"success": False, "reason": "no net specified."}) + net = data["net"] + if not net or net == "None": + nets = main.network.keys() + else: + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + nets = [net] + for net_name in nets: + conns = helpers.get_connected_relays(net_name) + if not conns: + return dumps({"success": False, "reason": f"failed to get instances for {net_name}."}) + if func == "recheckauth": + for conn in conns: + conn.regPing(reset=True) + elif func == "resetauth": + for conn in conns: + conn.authenticated = False + conn.regPing(reset=True) + elif func == "register": + for conn in conns: + regproc.registerAccount(conn.net, conn.num) + return dumps({"success": True}) + + @app.route("/irc/network//", methods=["POST"]) + @login_required + def irc_network(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + inst = main.network[net] + network = {} + network["net"] = inst.net + network["auth"] = inst.auth + network["host"] = inst.host + network["last"] = inst.last + network["port"] = inst.port + network["security"] = inst.security + network["relays"] = len(inst.relays) + network["chanlimit"] = inst.chanlimit + network["channels"] = userinfo.getTotalChanNum(net) + network["records"] = userinfo.getNumWhoEntries(net) + return dumps(network) + + @app.route("/irc/network//", methods=["DELETE"]) + @login_required + def irc_network_delete(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + main.network[net].seppuku() + del main.network[net] + main.saveConf("network") + return dumps({"success": True}) + + @app.route("/irc/network//edit/", methods=["POST"]) + @login_required + def irc_network_edit(self, request, net): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + inst = main.network[net] + for item in data: + if item == "auth": + auth = data[item][0] + if auth not in ["sasl", "ns", "none"]: + return dumps({"success": False, "reason": "invalid auth."}) + elif item == "host": + host = data[item][0] + elif item == "last": + last = data[item][0] + if not last.isdigit(): + return dumps({"success": False, "reason": "invalid last: not a number."}) + elif item == "port": + port = data[item][0] + if not port.isdigit(): + return dumps({"success": False, "reason": "invalid port: not a number."}) + port = int(port) + elif item == "chanlimit": + chanlimit = data[item][0] + if chanlimit == "None": + chanlimit = None + elif not chanlimit.isdigit(): + return dumps({"success": False, "reason": "invalid chanlimit: not a number."}) + else: + chanlimit = int(chanlimit) + online_relays = helpers.get_connected_relays(net) + for r in online_relays: + r.chanlimit = chanlimit + elif item == "security": + security = data[item][0] + if security not in ["ssl", "plain"]: + return dumps({"success": False, "reason": "invalid security."}) + inst.auth = auth + inst.host = host + inst.last = last + inst.port = port + inst.security = security + inst.chanlimit = chanlimit + main.saveConf("network") + + return dumps({"success": True}) + + @app.route("/irc/network/create/", methods=["PUT"]) + @login_required + def irc_network_create(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + fields = ["net", "auth", "host", "port", "security"] + if not set(fields).issubset(set(data)): + return dumps({"success": False, "reason": "not enough fields."}) + for item in data: + if item == "net": + net = data[item] + if net in main.network.keys(): + return dumps({"success": False, "reason": "network already exists."}) + if set(net).intersection(set(digits)): + return dumps({"success": False, "reason": "network name cannot contain numbers."}) + elif item == "auth": + auth = data[item] + if auth not in ["sasl", "ns", "none"]: + return dumps({"success": False, "reason": "invalid auth."}) + elif item == "host": + host = data[item] + elif item == "port": + port = data[item] + if not port.isdigit(): + return dumps({"success": False, "reason": "invalid port: not a number."}) + port = int(port) + elif item == "security": + security = data[item] + if security not in ["ssl", "plain"]: + return dumps({"success": False, "reason": "invalid security."}) + main.network[net] = Network(net, host, int(port), security, auth) + main.saveConf("network") + + return dumps({"success": True}) + + @app.route("/irc/network//relays/", methods=["POST"]) + @login_required + def irc_network_relays(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + relays_inst = main.network[net].relays + relays = [] + for num in relays_inst.keys(): + to_append = relays_inst[num] + name = f"{net}{num}" + if name in main.IRCPool.keys(): + to_append["chans"] = len(main.IRCPool[name].channels) + to_append["nick"] = main.IRCPool[name].nickname + to_append["conn"] = main.IRCPool[name].isconnected + to_append["limit"] = main.IRCPool[name].chanlimit + to_append["authed"] = main.IRCPool[name].authenticated + else: + to_append["chans"] = 0 + to_append["nick"] = None + to_append["conn"] = False + to_append["limit"] = None + to_append["authed"] = None + relays.append(to_append) + return dumps({"relays": relays}) + + @app.route("/irc/network///", methods=["POST"]) + @login_required + def irc_network_relay(self, request, net, num): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + net_inst = main.network[net] + if num not in net_inst.relays: + return dumps({"success": False, "reason": "network has no such relay."}) + if "status" in data: + if not type(data["status"]) == int: + return dumps({"success": False, "reason": "invalid type for enabled."}) + enabled = data["status"] + if enabled: + net_inst.enable_relay(num) + else: + net_inst.disable_relay(num) + main.saveConf("network") + return dumps({"success": True}) + + @app.route("/irc/network///", methods=["PUT"]) + @login_required + def irc_network_relay_add(self, request, net, num): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + net_inst = main.network[net] + if num in net_inst.relays: + return dumps({"success": False, "reason": "network already has this relay."}) + id, alias = net_inst.add_relay(num) + main.saveConf("network") + return dumps({"success": True, "id": id, "alias": alias}) + + @app.route("/irc/network//", methods=["PUT"]) + @login_required + def irc_network_relay_add_next(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + net_inst = main.network[net] + id, alias = net_inst.add_relay() + main.saveConf("network") + return dumps({"success": True, "id": id, "alias": alias}) + + @app.route("/irc/network///", methods=["DELETE"]) + @login_required + def irc_network_relay_del(self, request, net, num): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + net_inst = main.network[net] + if num not in net_inst.relays: + return dumps({"success": False, "reason": "network does not have this relay."}) + net_inst.delete_relay(num) + main.saveConf("network") + return dumps({"success": True}) + + @app.route("/irc/network///provision/", methods=["POST"]) + @login_required + def irc_network_relay_provision(self, request, net, num): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + net_inst = main.network[net] + if num not in net_inst.relays: + return dumps({"success": False, "reason": "network does not have this relay."}) + provision.provisionRelay(num, net) + return dumps({"success": True}) + + @app.route("/irc/network//channels/", methods=["POST"]) + @login_required + def irc_network_channels(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + relays_inst = main.network[net].relays + channels = [] + for num in relays_inst.keys(): + name = f"{net}{num}" + if name in main.IRCPool.keys(): + net_chans = main.IRCPool[name].channels + channels_annotated = userinfo.getUserNum(net, net_chans) + for channel in net_chans: + channel_inst = {"name": channel, "users": channels_annotated[channel], "num": num} + channels.append(channel_inst) + # channels[channel] = channels_annotated[channel] + + return dumps({"channels": channels}) + + # API version + @app.route("/irc/network//channel/", methods=["DELETE"]) + @login_required + def irc_network_channel_part_api(self, request, net): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "channel" not in data: + return dumps({"success": False, "reason": "no channel specified."}) + channel = data["channel"] + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + parted = chankeep.partSingle(net, channel) + if not parted: + dumps({"success": False, "reason": "no channels matched."}) + return dumps({"success": True, "relays": parted}) + + # API version + @app.route("/irc/network//channel/", methods=["PUT"]) + @login_required + def irc_network_channel_join_api(self, request, net): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "channel" not in data: + return dumps({"success": False, "reason": "no channel specified."}) + channel = data["channel"] + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + joined = chankeep.joinSingle(net, channel) + if not joined: + return dumps({"success": False, "reason": "could not allocate channel to relay."}) + return dumps({"success": True, "relays": joined}) + + @app.route("/irc/network//channel//", methods=["PUT"]) + @login_required + def irc_network_channel_join(self, request, net, channel): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + joined = chankeep.joinSingle(net, channel) + if not joined: + return dumps({"success": False, "reason": "could not allocate channel to relay."}) + return dumps({"success": True, "relays": joined}) + + @app.route("/aliases/", methods=["GET"]) + @login_required + def aliases(self, request): + alias_list = [] + for num, alias in main.alias.items(): + alias_dup = dict(alias) + alias_dup["num"] = num + alias_list.append(alias_dup) + + return dumps({"aliases": alias_list}) + + @app.route("/aliases/", methods=["POST"]) + @login_required + def aliases_update(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + for alias, fields in data.items(): + if not alias.isdigit(): + return dumps({"success": False, "reason": "alias not a number."}) + alias = int(alias) + if alias not in main.alias.keys(): + return dumps({"success": False, "reason": "alias does not exist."}) + if fields: + for field in fields: + if field in main.alias[alias]: + main.alias[alias][field] = fields[field] + if "emails" in fields: + if not fields["emails"]: + main.alias[alias]["emails"] = [] + main.saveConf("alias") + + return dumps({"success": True}) + + @app.route("/irc/auto//", methods=["POST"]) + @login_required + def irc_auto_network(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if 1 in main.network[net].relays.keys(): + return dumps({"success": False, "reason": f"first relay exists on {net}"}) + num, alias = main.network[net].add_relay(1) + provision.provisionRelay(num, net) + main.saveConf("network") + + return dumps({"success": True, "message": f"created relay {num} with alias {alias} on {net}"}) + + @app.route("/irc/list//", methods=["POST"]) + @login_required + def irc_list_network(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + first_relay = helpers.get_first_relay(net) + if not first_relay: + return dumps({"success": False, "reason": f"could not get first relay for {net}"}) + first_relay.list() + return dumps({"success": True, "message": f"requested list with instance {first_relay.num} of {net}"}) + + @app.route("/irc/list//", methods=["GET"]) + @login_required + def get_irc_list_info(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + listinfo = chankeep.getListInfo(net) + return dumps({"success": True, "listinfo": listinfo}) + + @app.route("/irc/msg///", methods=["PUT"]) + @login_required + def irc_send_message(self, request, net, num): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + if "msg" not in data: + return dumps({"success": False, "reason": "no msg."}) + if "channel" not in data: + return dumps({"success": False, "reason": "no msg."}) + msg = data["msg"] + channel = data["channel"] + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + if num not in main.network[net].relays.keys(): + return dumps({"success": False, "reason": f"no relay {num} on {net}"}) + name = f"{net}{num}" + if name not in main.IRCPool.keys(): + return dumps({"success": False, "reason": f"relay {num} not on {net}"}) + # We are in a query + in_query = False + if "nick" in data: + nick = data["nick"] + if nick == channel: + in_query = True + else: + nick = None + if channel == main.IRCPool[name].nickname or nick == channel: + in_query = True + if not nick: + return dumps({"success": False, "reason": "no nick specified to query"}) + else: + main.IRCPool[name].sendmsg(nick, msg, in_query=in_query) + else: + main.IRCPool[name].sendmsg(channel, msg, in_query=in_query) + return dumps({"success": True, "message": f"sent message to {channel} on {name}"}) + + @app.route("/irc/nick///", methods=["GET"]) + @login_required + def irc_get_nick(self, request, net, num): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + if num not in main.network[net].relays.keys(): + return dumps({"success": False, "reason": f"no relay {num} on {net}"}) + name = f"{net}{num}" + if name not in main.IRCPool.keys(): + return dumps({"success": False, "reason": f"relay {num} not on {net}"}) + return dumps({"nickname": main.IRCPool[name].nickname}) + + @app.route("/irc/reg//", methods=["GET"]) + @login_required + def irc_get_unreg_net(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + unreg = regproc.get_unregistered_relays(net) + return dumps({"success": True, "unreg": unreg}) + + @app.route("/irc/reg/", methods=["GET"]) + @login_required + def irc_get_unreg(self, request): + unreg = regproc.get_unregistered_relays() + return dumps({"success": True, "unreg": unreg}) + + @app.route("/irc/reg/", methods=["PUT"]) + @login_required + def irc_confirm_accounts(self, request): + try: + data = loads(request.content.read()) + except JSONDecodeError: + return "Invalid JSON" + for item, token in data.items(): + if "|" not in item: + return dumps({"success": False, "reason": f"malformed item: {item}"}) + spl = item.split("|") + if not len(spl) == 2: + return dumps({"success": False, "reason": f"malformed item: {item}"}) + net, num = spl + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + if token: + regproc.confirmAccount(net, num, token) + return dumps({"success": True}) + + @app.route("/irc/network///auth/", methods=["POST"]) + @login_required + def irc_enable_auth(self, request, net, num): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + if not num.isdigit(): + return dumps({"success": False, "reason": "invalid num: not a number."}) + num = int(num) + if num not in main.network[net].relays.keys(): + return dumps({"success": False, "reason": f"no relay {num} on {net}"}) + regproc.enableAuthentication(net, num, jump=False, run_now=True) + return dumps({"success": True}) + + @app.route("/irc/sinst//", methods=["GET"]) + @login_required + def irc_get_authentity(self, request, net): + if net not in main.network.keys(): + return dumps({"success": False, "reason": "no such net."}) + auth = regproc.selectInst(net) + if not auth: + return dumps({"success": False, "reason": "error getting results."}) + return dumps({"success": True, "sinst": auth}) diff --git a/legacy/commands/__init__.py b/legacy/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/legacy/commands/admall.py b/legacy/commands/admall.py new file mode 100644 index 0000000..9ae8636 --- /dev/null +++ b/legacy/commands/admall.py @@ -0,0 +1,23 @@ +import main +from utils.deliver_relay_commands import deliverRelayCommands + + +class AdmallCommand: + def __init__(self, *args): + self.admall(*args) + + def admall(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length > 2: + for i in main.network.keys(): + for x in main.network[i].relays.keys(): + num = main.network[i].relays[x]["id"] + commands = {spl[1]: [" ".join(spl[2:])]} + success("Sending commands to relay %s" % (num)) + deliverRelayCommands(num, commands) + return + else: + incUsage("admall") + return + else: + incUsage(None) diff --git a/legacy/commands/alias.py b/legacy/commands/alias.py new file mode 100644 index 0000000..fd5e644 --- /dev/null +++ b/legacy/commands/alias.py @@ -0,0 +1,54 @@ +from yaml import dump + +import main +from modules import alias + + +class AliasCommand: + def __init__(self, *args): + self.alias(*args) + + def alias(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + info(dump(main.alias)) + return + elif length == 3: + if spl[1] == "add": + if not spl[2].isdigit(): + failure("Must be a number, not %s" % spl[2]) + return + num = int(spl[2]) + for i in range(num): + if len(main.alias.keys()) == 0: + nextNum = 1 + else: + nextNum = max(main.alias.keys()) + 1 + main.alias[nextNum] = alias.generate_alias() + success("Generated new alias: %i" % nextNum) + main.saveConf("alias") + return + elif spl[1] == "del": + if not spl[2].isdigit(): + failure("Must be a number, not %s" % spl[2]) + return + num = int(spl[2]) + if num not in main.alias.keys(): + failure("No such alias: %i" % num) + return + failed = False + for i in main.network.keys(): + if num in main.network[i].aliases.keys(): + failure("Alias in use by %s" % i) + failed = True + if failed: + return + del main.alias[num] + success("Removed alias: %i" % num) + main.saveConf("alias") + return + else: + incUsage("alias") + return + else: + incUsage(None) diff --git a/legacy/commands/all.py b/legacy/commands/all.py new file mode 100644 index 0000000..509f0f5 --- /dev/null +++ b/legacy/commands/all.py @@ -0,0 +1,25 @@ +import main +from utils.deliver_relay_commands import deliverRelayCommands + + +class AllCommand: + def __init__(self, *args): + self.all(*args) + + def all(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length > 2: + for i in main.network.keys(): + for x in main.network[i].relays.keys(): + num = main.network[i].relays[x]["id"] + net = main.network[i].relays[x]["net"] + alias = main.alias[x]["nick"] + commands = {spl[1]: [" ".join(spl[2:])]} + success("Sending commands to relay %s as user %s" % (num, alias + "/" + net)) + deliverRelayCommands(num, commands, user=alias + "/" + net) + return + else: + incUsage("all") + return + else: + incUsage(None) diff --git a/legacy/commands/allc.py b/legacy/commands/allc.py new file mode 100644 index 0000000..cd55596 --- /dev/null +++ b/legacy/commands/allc.py @@ -0,0 +1,43 @@ +import main +from utils.deliver_relay_commands import deliverRelayCommands + + +class AllcCommand: + def __init__(self, *args): + self.allc(*args) + + def allc(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length > 4: + targets = [] + if spl[1] == "network": + for i in main.network.keys(): + for x in main.network[i].relays.keys(): + if main.network[i].relays[x]["net"] == spl[2]: + targets.append((i, x)) + elif spl[1] == "alias": + for i in main.network.keys(): + [ + targets.append((i, x)) + for x in main.alias.keys() + if main.alias[x]["nick"] == spl[2] and x in main.network[i].aliases.keys() + ] + else: + incUsage("allc") + return + if len(targets) == 0: + failure("No matches found: %s" % spl[2]) + return + for i in targets: + net = i[0] + num = i[1] + alias = main.alias[num]["nick"] + commands = {spl[3]: [" ".join(spl[4:])]} + success("Sending commands to relay %i as user %s" % (num, alias + "/" + net)) + deliverRelayCommands(num, commands, user=alias + "/" + net) + return + else: + incUsage("allc") + return + else: + incUsage(None) diff --git a/legacy/commands/authcheck.py b/legacy/commands/authcheck.py new file mode 100644 index 0000000..26cc14e --- /dev/null +++ b/legacy/commands/authcheck.py @@ -0,0 +1,37 @@ +import main + + +class AuthcheckCommand: + def __init__(self, *args): + self.authcheck(*args) + + def authcheck(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + results = [] + for i in main.IRCPool.keys(): + num = main.IRCPool[i].num + net = main.IRCPool[i].net + if not main.IRCPool[i].authenticated: + results.append("%s - %s: %s" % (net, num, main.alias[num]["nick"])) + info("\n".join(results)) + return + elif length == 2: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + results = [] + for i in main.IRCPool.keys(): + num = main.IRCPool[i].num + net = main.IRCPool[i].net + if not net == spl[1]: + continue + if not main.IRCPool[i].authenticated: + results.append("%s - %s: %s" % (net, num, main.alias[num]["nick"])) + info("\n".join(results)) + return + else: + incUsage("authcheck") + return + else: + incUsage(None) diff --git a/legacy/commands/auto.py b/legacy/commands/auto.py new file mode 100644 index 0000000..ffc6df5 --- /dev/null +++ b/legacy/commands/auto.py @@ -0,0 +1,39 @@ +import main +from modules import provision + + +class AutoCommand: + def __init__(self, *args): + self.auto(*args) + + def auto(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + for i in main.network.keys(): + if 1 in main.network[i].relays.keys(): + info("Skipping %s - first relay exists" % i) + else: + num, alias = main.network[i].add_relay(1) + success("Successfully created first relay on network %s with alias %s" % (i, alias)) + provision.provisionRelay(num, i) + success("Started provisioning network %s on first relay for alias %s" % (i, alias)) + main.saveConf("network") + return + elif length == 2: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + if 1 in main.network[spl[1]].relays.keys(): + failure("First relay exists on %s" % spl[1]) + return + num, alias = main.network[spl[1]].add_relay(1) + success("Successfully created relay %i on network %s with alias %s" % (num, spl[1], alias)) + provision.provisionRelay(num, spl[1]) + success("Started provisioning network %s on relay %s for alias %s" % (spl[1], num, alias)) + main.saveConf("network") + return + else: + incUsage("auto") + return + else: + incUsage(None) diff --git a/legacy/commands/blacklist.py b/legacy/commands/blacklist.py new file mode 100644 index 0000000..7dc3c19 --- /dev/null +++ b/legacy/commands/blacklist.py @@ -0,0 +1,43 @@ +from yaml import dump + +import main + + +class BlacklistCommand: + def __init__(self, *args): + self.blacklist(*args) + + def blacklist(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + info(dump(main.blacklist)) + return + elif length == 4: + if spl[1] == "add": + if spl[2] in main.blacklist.keys(): + main.blacklist[spl[2]].append(spl[3]) + else: + main.blacklist[spl[2]] = [spl[3]] + success("Blacklisted %s on %s" % (spl[3], spl[2])) + main.saveConf("blacklist") + return + elif spl[1] == "del": + if spl[2] in main.blacklist.keys(): + if spl[3] in main.blacklist[spl[2]]: + main.blacklist[spl[2]].remove(spl[3]) + if len(main.blacklist[spl[2]]) == 0: + del main.blacklist[spl[2]] + else: + failure("Not in list: %s" % spl[3]) + return + else: + failure("No such entry: %s" % spl[2]) + return + success("Removed blacklist %s on %s" % (spl[3], spl[2])) + main.saveConf("blacklist") + return + else: + incUsage("blacklist") + return + else: + incUsage(None) diff --git a/legacy/commands/chans.py b/legacy/commands/chans.py new file mode 100644 index 0000000..663d82a --- /dev/null +++ b/legacy/commands/chans.py @@ -0,0 +1,25 @@ +import modules.userinfo as userinfo + + +class ChansCommand: + def __init__(self, *args): + self.chans(*args) + + def chans(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if len(spl) < 2: + incUsage("chans") + return + result = userinfo.getChans(spl[1:]) + rtrn = "" + for i in result.keys(): + rtrn += "Matches from: %s" % i + rtrn += "\n" + for x in result[i]: + rtrn += x + rtrn += "\n" + rtrn += "\n" + info(rtrn) + return + else: + incUsage(None) diff --git a/legacy/commands/cmd.py b/legacy/commands/cmd.py new file mode 100644 index 0000000..e269da2 --- /dev/null +++ b/legacy/commands/cmd.py @@ -0,0 +1,22 @@ +from utils.deliver_relay_commands import deliverRelayCommands + + +class CmdCommand: + def __init__(self, *args): + self.cmd(*args) + + def cmd(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length > 4: + if not spl[1].isdigit(): + failure("Must be a number, not %s" % spl[1]) + return + commands = {spl[3]: [" ".join(spl[4:])]} + success("Sending commands to relay %s as user %s" % (spl[2], spl[3])) + deliverRelayCommands(int(spl[1]), commands, user=spl[2]) + return + else: + incUsage("cmd") + return + else: + incUsage(None) diff --git a/legacy/commands/confirm.py b/legacy/commands/confirm.py new file mode 100644 index 0000000..86d657c --- /dev/null +++ b/legacy/commands/confirm.py @@ -0,0 +1,28 @@ +import main +from modules import regproc + + +class ConfirmCommand: + def __init__(self, *args): + self.confirm(*args) + + def confirm(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 4: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + if not spl[2].isdigit(): + failure("Must be a number, not %s" % spl[2]) + return + if not int(spl[2]) in main.network[spl[1]].relays.keys(): + failure("No such relay on %s: %s" % (spl[1], spl[2])) + return + regproc.confirmAccount(spl[1], int(spl[2]), spl[3]) + success("Requested confirmation on %s - %s with token %s" % (spl[1], spl[2], spl[3])) + return + else: + incUsage("confirm") + return + else: + incUsage(None) diff --git a/legacy/commands/dedup.py b/legacy/commands/dedup.py new file mode 100644 index 0000000..0a286fc --- /dev/null +++ b/legacy/commands/dedup.py @@ -0,0 +1,25 @@ +from json import dumps + +import main +from modules import chankeep + + +class DedupCommand: + def __init__(self, *args): + self.dedup(*args) + + def dedup(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + dupes = chankeep.getDuplicateChannels() + chankeep.partChannels(dupes) + info(dumps(dupes)) + return + elif length == 2: + if spl[1] not in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + dupes = chankeep.getDuplicateChannels(spl[1]) + chankeep.partChannels(dupes) + info(dumps(dupes)) + return diff --git a/legacy/commands/disable.py b/legacy/commands/disable.py new file mode 100644 index 0000000..78b0984 --- /dev/null +++ b/legacy/commands/disable.py @@ -0,0 +1,46 @@ +import main +from utils.deliver_relay_commands import deliverRelayCommands + + +class DisableCommand: + def __init__(self, *args): + self.disable(*args) + + def disable(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 3: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + if not spl[2].isdigit(): + failure("Must be a number, not %s" % spl[2]) + return + relayNum = int(spl[2]) + name = spl[1] + spl[2] + if not spl[1] in main.IRCPool.keys(): + info("Note - instance not running, proceeding anyway") + if relayNum not in main.network[spl[1]].relays.keys(): + failure("No such relay: %s in network %s" % (spl[2], spl[1])) + return + main.network[spl[1]].relays[relayNum]["enabled"] = False + user = main.alias[relayNum]["nick"] + network = spl[1] + # relay = main.network[spl[1]].relays[relayNum] + commands = {"status": ["Disconnect"]} + deliverRelayCommands(relayNum, commands, user=user + "/" + network) + main.saveConf("network") + if name in main.ReactorPool.keys(): + if name in main.FactoryPool.keys(): + main.FactoryPool[name].stopTrying() + main.ReactorPool[name].disconnect() + if name in main.IRCPool.keys(): + del main.IRCPool[name] + del main.ReactorPool[name] + del main.FactoryPool[name] + success("Successfully disabled bot %s on network %s" % (spl[2], spl[1])) + return + else: + incUsage("disable") + return + else: + incUsage(None) diff --git a/legacy/commands/dist.py b/legacy/commands/dist.py new file mode 100644 index 0000000..a126e07 --- /dev/null +++ b/legacy/commands/dist.py @@ -0,0 +1,22 @@ +from subprocess import PIPE, run + +import main + + +class DistCommand: + def __init__(self, *args): + self.dist(*args) + + def dist(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if main.config["Dist"]["Enabled"]: + rtrn = run([main.config["Dist"]["File"]], shell=True, stdout=PIPE) + if main.config["Dist"]["SendOutput"]: + info("Exit code: %s -- Stdout: %s" % (rtrn.returncode, rtrn.stdout)) + else: + info("Exit code: %s" % rtrn.returncode) + else: + failure("The dist command is not enabled") + return + else: + incUsage(None) diff --git a/legacy/commands/email.py b/legacy/commands/email.py new file mode 100644 index 0000000..fa99fa0 --- /dev/null +++ b/legacy/commands/email.py @@ -0,0 +1,93 @@ +from yaml import dump + +import main + + +class EmailCommand: + def __init__(self, *args): + self.email(*args) + + def email(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 4: + if spl[1] == "add": + if spl[2].isdigit(): + num = int(spl[2]) + if num not in main.alias.keys(): + failure("No such alias: %i" % num) + return + if not spl[3] in main.alias[num]["emails"]: + main.alias[num]["emails"].append(spl[3]) + main.saveConf("alias") + success("Successfully added email %s to alias %i" % (spl[3], num)) + return + else: + failure("Email already exists in alias %i: %s" % (num, spl[3])) + return + else: + failure("Must be a number, not %s" % spl[2]) + if spl[2] == "domain": + domain = spl[3] + if "@" in domain: + failure("Not a domain: %s" % domain) + return + if domain not in main.irc["_"]["domains"]: + main.irc["_"]["domains"].append(domain) + success("Successfully added domain %s to default config" % domain) + main.saveConf("irc") + else: + failure("Domain already exists in default config: %s" % domain) + return + + elif spl[1] == "del": + if not spl[2].isdigit(): + # failure("Must be a number, not %s" % spl[2]) + if spl[2] == "domain": + domain = spl[3] + + if domain in main.irc["_"]["domains"]: + main.irc["_"]["domains"].remove(domain) + success("Successfully removed domain %s to default config" % domain) + main.saveConf("irc") + else: + failure("Domain does not exist in default config: %s" % domain) + return + + elif spl[1] == "del": + if num not in main.alias.keys(): + failure("No such alias: %i" % num) + return + if spl[3] in main.alias[num]["emails"]: + main.alias[num]["emails"].remove(spl[3]) + main.saveConf("alias") + success("Successfully removed email %s from alias %i" % (spl[3], num)) + return + else: + failure("Email does not exist in alias %i: %s" % (spl[3], num)) + return + elif length == 2: + if spl[1] == "list": + info(dump(main.alias)) + return + else: + incUsage("email") + return + elif length == 3: + if spl[1] == "list": + if spl[2] == "domain": + filtered = { + f"{k}:{k2}": v2 for k, v in main.irc.items() for k2, v2 in v.items() if k2 == "domains" + } + info(dump(filtered)) + return + else: + incUsage("email") + return + else: + incUsage("email") + return + else: + incUsage("email") + return + else: + incUsage(None) diff --git a/legacy/commands/enable.py b/legacy/commands/enable.py new file mode 100644 index 0000000..3553446 --- /dev/null +++ b/legacy/commands/enable.py @@ -0,0 +1,38 @@ +import main +from utils.deliver_relay_commands import deliverRelayCommands + + +class EnableCommand: + def __init__(self, *args): + self.enable(*args) + + def enable(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 3: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + if not spl[2].isdigit(): + failure("Must be a number, not %s" % spl[2]) + return + if not int(spl[2]) in main.network[spl[1]].relays.keys(): + failure("No such relay on %s: %s" % (spl[2], spl[1])) + return + + main.network[spl[1]].relays[int(spl[2])]["enabled"] = True + user = main.alias[int(spl[2])]["nick"] + network = spl[1] + commands = {"status": ["Connect"]} + deliverRelayCommands(int(spl[2]), commands, user=user + "/" + network) + main.saveConf("network") + if not spl[1] + spl[2] in main.IRCPool.keys(): + main.network[spl[1]].start_bot(int(spl[2])) + else: + pass + success("Successfully enabled bot %s on network %s" % (spl[2], spl[1])) + return + else: + incUsage("enable") + return + else: + incUsage(None) diff --git a/legacy/commands/exec.py b/legacy/commands/exec.py new file mode 100644 index 0000000..9b7ea28 --- /dev/null +++ b/legacy/commands/exec.py @@ -0,0 +1,19 @@ +class ExecCommand: + def __init__(self, *args): + self.exec(*args) + + def exec(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length > 1: + try: + rtrn = exec(" ".join(spl[1:])) + except Exception as err: + failure(str(err)) + return + info(str(rtrn)) + return + else: + incUsage("exec") + return + else: + incUsage(None) diff --git a/legacy/commands/getstr.py b/legacy/commands/getstr.py new file mode 100644 index 0000000..c4fe6a9 --- /dev/null +++ b/legacy/commands/getstr.py @@ -0,0 +1,33 @@ +import main +from utils.get import getRelay + + +class GetstrCommand: + def __init__(self, *args): + self.getstr(*args) + + def getstr(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 3: + net = spl[1] + num = spl[2] + if net not in main.network.keys(): + failure("Network does not exist: %s" % net) + return + if not num.isdigit(): + failure("Must be a number, not %s" % num) + return + num = int(num) + alias = main.alias[num]["nick"].lower() + host, port = getRelay(num) + password = main.config["Relay"]["Password"] + connstr = f"/connect -ssl {host} {port}" + authstr = f"/quote PASS {alias}/{net}:{password}" + obj.send(connstr) + obj.send(authstr) + + else: + incUsage("getstr") + return + else: + incUsage(None) diff --git a/legacy/commands/help.py b/legacy/commands/help.py new file mode 100644 index 0000000..6dcf937 --- /dev/null +++ b/legacy/commands/help.py @@ -0,0 +1,16 @@ +import main + + +class HelpCommand: + def __init__(self, *args): + self.help(*args) + + def help(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + helpMap = [] + for i in main.help.keys(): + helpMap.append("%s: %s" % (i, main.help[i])) + info("\n".join(helpMap)) + return + else: + incUsage(None) diff --git a/legacy/commands/join.py b/legacy/commands/join.py new file mode 100644 index 0000000..e6cbc20 --- /dev/null +++ b/legacy/commands/join.py @@ -0,0 +1,48 @@ +import main +import modules.chankeep + + +class JoinCommand: + def __init__(self, *args): + self.join(*args) + + def join(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 3: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + modules.chankeep.joinSingle(spl[1], spl[2]) + success("Joined %s" % spl[2]) + + elif length == 4: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + if not int(spl[2]) in main.network[spl[1]].relays.keys(): + failure("Relay %s does not exist on network %s" % (spl[2], spl[1])) + return + if not spl[1] + spl[2] in main.IRCPool.keys(): + failure("Name has no instance: %s" % spl[1]) + return + main.IRCPool[spl[1] + spl[2]].join(spl[3]) + success("Joined %s" % spl[3]) + return + elif length == 5: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + if not int(spl[2]) in main.network[spl[1]].relays.keys(): + failure("Relay % does not exist on network %", (spl[2], spl[1])) + return + if not spl[1] + spl[2] in main.IRCPool.keys(): + failure("Name has no instance: %s" % spl[1]) + return + main.IRCPool[spl[1] + spl[2]].join(spl[3], spl[4]) + success("Joined %s with key %s" % (spl[3], spl[4])) + return + else: + incUsage("join") + return + else: + incUsage(None) diff --git a/legacy/commands/list.py b/legacy/commands/list.py new file mode 100644 index 0000000..9dccb8b --- /dev/null +++ b/legacy/commands/list.py @@ -0,0 +1,36 @@ +import main +from modules import helpers + + +class ListCommand: + def __init__(self, *args): + self.list(*args) + + def list(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + for i in main.network.keys(): + first_relay = helpers.get_first_relay(i) + #### + if not first_relay: + info("Network has no first instance: %s" % i) + continue + first_relay.list() + success(f"Requested list with instance {first_relay.num} of {i}") + return + elif length == 2: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + first_relay = helpers.get_first_relay(spl[1]) + if not first_relay: + failure("Could not get first instance") + return + first_relay.list() + success(f"Requested list with instance {first_relay.num} of {spl[1]}") + return + else: + incUsage("list") + return + else: + incUsage(None) diff --git a/legacy/commands/load.py b/legacy/commands/load.py new file mode 100644 index 0000000..31e66f4 --- /dev/null +++ b/legacy/commands/load.py @@ -0,0 +1,30 @@ +import main + + +class LoadCommand: + def __init__(self, *args): + self.load(*args) + + def load(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 2: + if spl[1] in main.filemap.keys(): + main.loadConf(spl[1]) + success("Loaded %s from %s" % (spl[1], main.filemap[spl[1]][0])) + return + elif spl[1] == "all": + for i in main.filemap.keys(): + main.loadConf(i) + success("Loaded %s from %s" % (i, main.filemap[i][0])) + return + elif spl[1] == "list": + info(", ".join(main.filemap.keys())) + return + else: + incUsage("load") + return + else: + incUsage("load") + return + else: + incUsage(None) diff --git a/legacy/commands/loadmod.py b/legacy/commands/loadmod.py new file mode 100644 index 0000000..0dbc8a8 --- /dev/null +++ b/legacy/commands/loadmod.py @@ -0,0 +1,26 @@ +from utils.loaders.single_loader import loadSingle + + +class LoadmodCommand: + def __init__(self, *args): + self.loadmod(*args) + + def loadmod(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 2: + rtrn = loadSingle(spl[1]) + if rtrn is True: + success("Loaded module: %s" % spl[1]) + return + elif rtrn == "RELOAD": + success("Reloaded module: %s" % spl[1]) + return + else: + failure("Error loading module %s: %s" % (spl[1], rtrn)) + return + else: + incUsage("loadmod") + return + else: + incUsage(None) + return diff --git a/legacy/commands/logout.py b/legacy/commands/logout.py new file mode 100644 index 0000000..776dfd0 --- /dev/null +++ b/legacy/commands/logout.py @@ -0,0 +1,11 @@ +class LogoutCommand: + def __init__(self, *args): + self.logout(*args) + + def logout(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + obj.authed = False + success("Logged out") + return + else: + incUsage(None) diff --git a/legacy/commands/mod.py b/legacy/commands/mod.py new file mode 100644 index 0000000..85e7814 --- /dev/null +++ b/legacy/commands/mod.py @@ -0,0 +1,48 @@ +import main + + +class ModCommand: + # This could be greatly improved, but not really important right now + def __init__(self, *args): + self.mod(*args) + + def mod(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 4: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + + try: + setattr(main.network[spl[1]], spl[2], spl[3]) + except Exception as e: + failure(f"Something went wrong: {e}") + return + + main.saveConf("network") + success("Successfully set key %s to %s on %s" % (spl[2], spl[3], spl[1])) + return + # Find a better way to do this + # elif length == 6: + # if not spl[1] in main.network.keys(): + # failure("Network does not exist: %s" % spl[1]) + # return + # if not spl[3].isdigit(): + # failure("Must be a number, not %s" % spl[3]) + # return + # if not int(spl[3]) in main.network[spl[1]].relays.keys(): + # failure("Relay/alias does not exist: %s" % spl[3]) + # return + + # try: + # x = getattr(main.network[spl[1]], spl[2]) + # x[spl[3]] = spl[4] + # except Exception as err: + # failure("Error: %s" % err) + # return + + else: + incUsage("mod") + return + else: + incUsage(None) diff --git a/legacy/commands/msg.py b/legacy/commands/msg.py new file mode 100644 index 0000000..452a532 --- /dev/null +++ b/legacy/commands/msg.py @@ -0,0 +1,29 @@ +import main + + +class MsgCommand: + def __init__(self, *args): + self.msg(*args) + + def msg(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length >= 5: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + if not int(spl[2]) in main.network[spl[1]].relays.keys(): + failure("Relay %s does not exist on network %s" % (spl[2], spl[1])) + return + if not spl[1] + spl[2] in main.IRCPool.keys(): + failure("Name has no instance: %s" % spl[1]) + return + if not spl[3] in main.IRCPool[spl[1] + spl[2]].channels: + info("Bot not on channel: %s" % spl[3]) + main.IRCPool[spl[1] + spl[2]].msg(spl[3], " ".join(spl[4:])) + success("Sent %s to %s on relay %s on network %s" % (" ".join(spl[4:]), spl[3], spl[2], spl[1])) + return + else: + incUsage("msg") + return + else: + incUsage(None) diff --git a/legacy/commands/network.py b/legacy/commands/network.py new file mode 100644 index 0000000..a1da160 --- /dev/null +++ b/legacy/commands/network.py @@ -0,0 +1,66 @@ +from string import digits + +from yaml import dump + +import main +from modules.network import Network + + +class NetworkCommand: + def __init__(self, *args): + self.network(*args) + + def network(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 7: + if spl[1] == "add": + if spl[2] in main.network.keys(): + failure("Network already exists: %s" % spl[2]) + return + if not spl[4].isdigit(): + failure("Port must be an integer, not %s" % spl[4]) + return + if not spl[5].lower() in ["ssl", "plain"]: + failure("Security must be ssl or plain, not %s" % spl[5]) + return + if set(spl[2]).intersection(set(digits)): + failure("Network name cannot contain numbers") + return + if not spl[6].lower() in ["sasl", "ns", "none"]: + failure("Auth must be sasl, ns or none, not %s" % spl[5]) + return + else: + main.network[spl[2]] = Network(spl[2], spl[3], int(spl[4]), spl[5].lower(), spl[6].lower()) + success("Successfully created network: %s" % spl[2]) + main.saveConf("network") + return + else: + incUsage("network") + return + + elif length == 3: + if spl[1] == "del": + if spl[2] in main.network.keys(): + main.network[spl[2]].seppuku() # ;( + del main.network[spl[2]] + success("Successfully removed network: %s" % spl[2]) + main.saveConf("network") + return + else: + failure("No such network: %s" % spl[2]) + return + else: + incUsage("network") + return + elif length == 2: + if spl[1] == "list": + info(dump(main.network)) + return + else: + incUsage("network") + return + else: + incUsage("network") + return + else: + incUsage(None) diff --git a/legacy/commands/part.py b/legacy/commands/part.py new file mode 100644 index 0000000..249c32a --- /dev/null +++ b/legacy/commands/part.py @@ -0,0 +1,27 @@ +import main + + +class PartCommand: + def __init__(self, *args): + self.part(*args) + + def part(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 4: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + if not int(spl[2]) in main.network[spl[1]].relays.keys(): + failure("Relay % does not exist on network %", (spl[2], spl[1])) + return + if not spl[1] + spl[2] in main.IRCPool.keys(): + failure("Name has no instance: %s" % spl[1] + spl[2]) + return + main.IRCPool[spl[1] + spl[2]].part(spl[3]) + success("Left %s" % spl[3]) + return + else: + incUsage("part") + return + else: + incUsage(None) diff --git a/legacy/commands/pass.py b/legacy/commands/pass.py new file mode 100644 index 0000000..e37a51e --- /dev/null +++ b/legacy/commands/pass.py @@ -0,0 +1,23 @@ +import main + + +class PassCommand: + def __init__(self, *args): + self.password(*args) + + def password(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + info("You are already authenticated") + return + else: + if length == 2: + if spl[1] == main.config["Password"]: + success("Authenticated successfully") + obj.authed = True + return + else: + failure("Password incorrect") + obj.transport.loseConnection() + return + else: + incUsage("pass") diff --git a/legacy/commands/pending.py b/legacy/commands/pending.py new file mode 100644 index 0000000..b65f2ad --- /dev/null +++ b/legacy/commands/pending.py @@ -0,0 +1,32 @@ +import main + + +class PendingCommand: + def __init__(self, *args): + self.pending(*args) + + def pending(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + results = [] + for i in main.network.keys(): + for x in main.network[i].relays.keys(): + if not main.network[i].relays[x]["registered"]: + results.append("%s: confirm %s %s [code]" % (main.alias[x]["nick"], i, x)) + info("\n".join(results)) + return + elif length == 2: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + results = [] + for x in main.network[spl[1]].relays.keys(): + if not main.network[spl[1]].relays[x]["registered"]: + results.append("%s: confirm %s %s [code]" % (main.alias[x]["nick"], spl[1], x)) + info("\n".join(results)) + return + else: + incUsage("pending") + return + else: + incUsage(None) diff --git a/legacy/commands/recheckauth.py b/legacy/commands/recheckauth.py new file mode 100644 index 0000000..7cdf527 --- /dev/null +++ b/legacy/commands/recheckauth.py @@ -0,0 +1,34 @@ +import main + + +class RecheckauthCommand: + def __init__(self, *args): + self.recheckauth(*args) + + def recheckauth(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + for i in main.IRCPool.keys(): + net = main.IRCPool[i].net + main.IRCPool[i].authenticated = False + main.IRCPool[i].regPing() + success("Successfully reset authentication status") + return + elif length == 2: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + for i in main.IRCPool.keys(): + # num = main.IRCPool[i].num + net = main.IRCPool[i].net + if not net == spl[1]: + continue + main.IRCPool[i].authenticated = False + main.IRCPool[i].regPing() + success("Successfully reset authentication status on %s" % spl[1]) + return + else: + incUsage("recheckauth") + return + else: + incUsage(None) diff --git a/legacy/commands/reg.py b/legacy/commands/reg.py new file mode 100644 index 0000000..41bb5f0 --- /dev/null +++ b/legacy/commands/reg.py @@ -0,0 +1,36 @@ +import main +from modules import regproc + + +class RegCommand: + def __init__(self, *args): + self.reg(*args) + + def reg(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 2: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + for i in main.network[spl[1]].relays.keys(): + regproc.registerAccount(spl[1], i) + success("Requested registration for all relays on %s" % spl[1]) + return + elif length == 3: + if not spl[1] in main.network.keys(): + failure("No such network: %s" % spl[1]) + return + if not spl[2].isdigit(): + failure("Must be a number, not %s" % spl[2]) + return + if not int(spl[2]) in main.network[spl[1]].relays.keys(): + failure("No such relay on %s: %s" % (spl[2], spl[1])) + return + regproc.registerAccount(spl[1], int(spl[2])) + success("Requested registration on %s - %s" % (spl[1], spl[2])) + return + else: + incUsage("reg") + return + else: + incUsage(None) diff --git a/legacy/commands/relay.py b/legacy/commands/relay.py new file mode 100644 index 0000000..2b80f5e --- /dev/null +++ b/legacy/commands/relay.py @@ -0,0 +1,66 @@ +from yaml import dump + +import main + + +class RelayCommand: + def __init__(self, *args): + self.relay(*args) + + def relay(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 3: + if spl[1] == "add": + if spl[2] in main.network.keys(): + id, alias = main.network[spl[2]].add_relay() + success("Successfully created relay %s on network %s with alias %s" % (str(id), spl[2], alias)) + main.saveConf("network") + return + else: + failure("No such network: %s" % spl[2]) + return + elif spl[1] == "list": + if spl[2] not in main.network.keys(): + failure("No such network: %s" % spl[2]) + return + info(dump(main.network[spl[2]].relays)) + return + else: + incUsage("relay") + return + + elif length == 4: + if spl[1] == "add": + if spl[2] in main.network.keys(): + if not spl[3].isdigit(): + failure("Must be a number, not %s" % spl[3]) + return + id, alias = main.network[spl[2]].add_relay(int(spl[3])) + success("Successfully created relay %s on network %s with alias %s" % (str(id), spl[2], alias)) + main.saveConf("network") + return + else: + failure("No such network: %s" % spl[2]) + return + elif spl[1] == "del": + if not spl[2] in main.network.keys(): + failure("No such network: %s" % spl[2]) + return + if not spl[3].isdigit(): + failure("Must be a number, not %s" % spl[3]) + return + if not int(spl[3]) in main.network[spl[2]].relays.keys(): + failure("No such relay: %s on network %s" % (spl[3], spl[2])) + return + main.network[spl[2]].delete_relay(int(spl[3])) + success("Successfully deleted relay %s on network %s" % (spl[3], spl[2])) + main.saveConf("network") + return + else: + incUsage("relay") + return + else: + incUsage("relay") + return + else: + incUsage(None) diff --git a/legacy/commands/save.py b/legacy/commands/save.py new file mode 100644 index 0000000..12624cd --- /dev/null +++ b/legacy/commands/save.py @@ -0,0 +1,29 @@ +import main + + +class SaveCommand: + def __init__(self, *args): + self.save(*args) + + def save(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 2: + if spl[1] in main.filemap.keys(): + main.saveConf(spl[1]) + success("Saved %s to %s" % (spl[1], main.filemap[spl[1]][0])) + return + elif spl[1] == "all": + for i in main.filemap.keys(): + main.saveConf(i) + success("Saved %s to %s" % (i, main.filemap[i][0])) + return + elif spl[1] == "list": + info(", ".join(main.filemap.keys())) + else: + incUsage("save") + return + else: + incUsage("save") + return + else: + incUsage(None) diff --git a/legacy/commands/stats.py b/legacy/commands/stats.py new file mode 100644 index 0000000..e2e8eb9 --- /dev/null +++ b/legacy/commands/stats.py @@ -0,0 +1,69 @@ +from string import digits + +import main +import modules.counters as count +import modules.userinfo as userinfo + + +class StatsCommand: + def __init__(self, *args): + self.stats(*args) + + def stats(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 1: + stats = [] + numChannels = 0 + numWhoEntries = 0 + for i in main.IRCPool.keys(): + numChannels += len(main.IRCPool[i].channels) + numWhoEntries += userinfo.getNumTotalWhoEntries() + stats.append("Registered servers:") + stats.append(" Unique: %s" % len(main.network.keys())) + stats.append("Online servers:") + stats.append(" Total: %s" % len(main.IRCPool.keys())) + stats.append(" Unique: %s" % len(main.liveNets())) + stats.append("Channels: %s" % numChannels) + stats.append("User records: %s" % numWhoEntries) + stats.append("Events/min: %s" % main.lastMinuteSample) + counterEvents = count.getEvents() + if counterEvents is None: + stats.append("No counters records") + else: + stats.append("Counters:") + for i in counterEvents.keys(): + stats.append(" %s: %s" % (i, counterEvents[i])) + info("\n".join(stats)) + return + + elif length == 2: + stats = [] + numChannels = 0 + numWhoEntries = 0 + found = False + numNodes = 0 + + for i in main.IRCPool.keys(): + if "".join([x for x in i if x not in digits]) == spl[1]: + numChannels += len(main.IRCPool[i].channels) + found = True + numNodes += 1 + if not found: + failure("Name does not exist: %s" % spl[1]) + numWhoEntries += userinfo.getNumWhoEntries(spl[1]) + stats.append("Channels: %s" % numChannels) + stats.append("User records: %s" % numWhoEntries) + stats.append("Endpoints: %s" % numNodes) + counterEvents = count.getEvents(spl[1]) + if counterEvents is None: + stats.append("No counters records") + else: + stats.append("Counters:") + for i in counterEvents.keys(): + stats.append(" %s: %s" % (i, counterEvents[i])) + info("\n".join(stats)) + return + else: + incUsage("stats") + else: + incUsage(None) diff --git a/legacy/commands/swho.py b/legacy/commands/swho.py new file mode 100644 index 0000000..18dbc12 --- /dev/null +++ b/legacy/commands/swho.py @@ -0,0 +1,44 @@ +import main + + +class SwhoCommand: + def __init__(self, *args): + self.swho(*args) + + def swho(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 2: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + for i in main.IRCPool.keys(): + if spl[1] == main.IRCPool[i].net: + for x in main.IRCPool[i].channels: + main.IRCPool[i].who(x) + success("Sent WHO to all channels on all networks for %s" % spl[1]) + return + elif length == 3: + if not spl[1] in main.network.keys(): + failure("Network does not exist: %s" % spl[1]) + return + matches = [] + + # This loop gets all networks where the core network matches spl[1] + # where there is also a currently joined channel matching spl[2] + for i in main.IRCPool.keys(): + if spl[1] == main.IRCPool[i].net: + for x in main.IRCPool[i].channels: + if x == spl[2]: + main.IRCPool[i].who(x) + matches.append(i) + if matches == []: + failure("No matches found for channel %s" % spl[2]) + return + success("Sent WHO to %s on: %s" % (spl[2], ", ".join(matches))) + return + + else: + incUsage("swho") + return + else: + incUsage(None) diff --git a/legacy/commands/token.py b/legacy/commands/token.py new file mode 100644 index 0000000..738f537 --- /dev/null +++ b/legacy/commands/token.py @@ -0,0 +1,60 @@ +from uuid import uuid4 + +from yaml import dump + +import main + + +class TokenCommand: + def __init__(self, *args): + self.token(*args) + + def token(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 2: + if spl[1] == "list": + info(dump(main.tokens)) + return + else: + incUsage("token") + return + elif length == 3: + if spl[1] == "del": + if spl[2] in main.tokens.keys(): + del main.tokens[spl[2]] + main.saveConf("tokens") + success("Successfully removed token: %s" % spl[2]) + return + else: + failure("No such token") + return + else: + incUsage("token") + return + elif length == 4: + if spl[1] == "add": + if not spl[2] in main.tokens.keys(): + if spl[3] in ["relay", "api"]: # more to come! + main.tokens[spl[2]] = { + "hello": str(uuid4()), + "usage": spl[3], + "counter": str(uuid4()), + } + main.saveConf("tokens") + success("Successfully created token %s:" % spl[2]) + info(dump(main.tokens[spl[2]])) + return + else: + incUsage("token") + return + else: + failure("Token already exists: %s" % spl[2]) + return + else: + incUsage("token") + return + else: + incUsage("token") + return + else: + incUsage(None) diff --git a/legacy/commands/users.py b/legacy/commands/users.py new file mode 100644 index 0000000..be7a7b1 --- /dev/null +++ b/legacy/commands/users.py @@ -0,0 +1,25 @@ +import modules.userinfo as userinfo + + +class UsersCommand: + def __init__(self, *args): + self.users(*args) + + def users(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if len(spl) < 2: + incUsage("users") + return + result = userinfo.getUsers(spl[1:]) + rtrn = "" + for i in result.keys(): + rtrn += "Matches from: %s" % i + rtrn += "\n" + for x in result[i]: + rtrn += x + rtrn += "\n" + rtrn += "\n" + info(rtrn) + return + else: + incUsage(None) diff --git a/legacy/commands/who.py b/legacy/commands/who.py new file mode 100644 index 0000000..10faa93 --- /dev/null +++ b/legacy/commands/who.py @@ -0,0 +1,26 @@ +import modules.userinfo as userinfo + + +class WhoCommand: + def __init__(self, *args): + self.who(*args) + + def who(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 2: + result = userinfo.getWho(spl[1]) + rtrn = "" + for i in result.keys(): + rtrn += "Matches from: %s" % i + rtrn += "\n" + for x in result[i]: + rtrn += x + rtrn += "\n" + rtrn += "\n" + info(rtrn) + return + else: + incUsage("who") + return + else: + incUsage(None) diff --git a/legacy/conf/templates/aliasdata.json b/legacy/conf/templates/aliasdata.json new file mode 100644 index 0000000..9fbc997 --- /dev/null +++ b/legacy/conf/templates/aliasdata.json @@ -0,0 +1 @@ +{"realnames":["Roslyn Capps","Vasiliki Mor","Tiffani Harlin","Jarrod Bobo","Ellamae Lenser","Jamal Coil","Kristan Baugher","Barton Bucklin","Francesco So","Shemeka Pickerel","Enola Running","Takisha Voegele","Yuko Stern","Lorraine Li","Deanna Marano","Pamula Wallis","Glory Hammock","Shenita Hajduk","Adrienne Pflum","Sherill Canton","Maxima Marsee","Oliva Kutcher","Digna Gugino","Liz Astudillo","Rico Mayorga","Dominga Fewell","Hannelore Serra","Carlene Rosario","Clifton Pasha","Adell Leffingwell","Jenise Koziel","Sharla Marlatt","Harvey Daoust","Keri Badger","Tyson Lev","Cathrine Otero","Kristofer Truman","Lauretta Bleau","Saundra Kimberly","Bobette Winford","Lawerence Tepper","Cordell Lagrone","Saran Koh","Christal Simmers","Leda Mendosa","Armand Simmon","Kara Macaulay","Pamala Gabrielson","Pearline Pierre","Erline Aguilar","Hilario Mcclusky","Donnette Stapleton","Jazmin Oberholtzer","Nicolle Tufts","Pam Kogut","Jaunita Wilkenson","Mozell Battaglia","Elke Viands","Erinn Siple","Jacklyn Rhodus","Harmony Obey","Malissa Chagolla","Betsey Broce","Nadene Cundiff","Natalia Kealey","Edmond Bowie","Sadie Koopman","Robbin Blunk","Afton Fling","Scot Wei","Creola Lauber","Hazel Stong","Fredricka Auxier","Cherilyn Terrazas","Kenna Wedding","Anthony Jacobo","Claudine Freels","Michelina Linz","Jonathon Lipari","Brendon Raposo","Inga Fullam","Valentin Freitas","Marhta Kime","Mikaela Hilty","Vania Viars","Sidney Schuyler","Yang Carlin","Candance Sandin","Felipe Engh","Tamara Everman","Zita Mershon","Eugena Chagoya","Willodean Yoo","Sydney Hulbert","Tatiana Tristan","Clarissa Smedley","Arline Struck","Kourtney Schulenberg","Van Gomez","Ellsworth Lozada","Ina Spires","Teisha Benedetti","Daniela Lauzon","Devora Dugan","Clifton Heinze","Charmaine Maxton","Delaine Pullum","Cindy Herrman","Catalina Sabine","Tomoko Morra","Edgar Flinchbaugh","Kelly Pleasants","David Munsell","Jeanette Obert","Shaunte Breault","Abby Maillet","Chad Tafolla","Lakiesha Wilber","Jim Cardillo","Robbie Milton","Mac Tibbets","Nancy Kirst","Kiley Masone","Cathy Ohearn","Minh Ahlstrom","Nannie Schillaci","Lane Leishman","King Beech","Agnes Reising","Katelynn Alton","Trudie Shelley","Johnsie Duda","Rosette Ruffo","Loreta Mor","Winston Johnson","Marilu Fabian","Romeo Reinhard","Johnnie Minder","Tasia Burgett","Fumiko Fuentez","Lesha Vowels","Savanna Toothman","Darwin Smithson","Rona Blazek","Gigi Wollard","Denny Gee","Norman Fellows","Noelle Palmieri","Lesli Ronan","Celestine Lor","Evan Briere","Dolly Kline","Cody Artman","Lakia Smothers","Luke Wagar","Larraine Hillin","Teresia Sedor","Luanne Broderick","Johnny Cable","Shirley Shorey","Colin Mancilla","Yevette Cisco","Barbra Schwenk","Dawn Heavrin","Lieselotte Stall","Yasmin Combs","Alla Ambler","Sebastian Shevlin","Stephan Kennington","Lorena Dardar","Gayla Zeledon","Etha Winslow","Marilynn Lazzaro","Julian Shiver","Emmie Criswell","Mae Demeritt","Buddy Nathaniel","Cruz Loring","Marita Callihan","Dorathy Mckennon","Xochitl Chappelle","Rebecka Pfarr","Branden Rafferty","Ozell Boutte","Ruben Madruga","Dwayne Delzell","Adolph Reinecke","Valery Kugel","Carolina Mcglown","Kelly Culver","Esmeralda Demo","Maricela Tandy","Micheline Oathout","Alec Newhouse","Leeanna Meggs","Kamilah Lundgren","Evette Nardi","Hattie Gallien","Isa Hagens","Jesus Matousek","Newton Primrose","Lazaro Morley","Racheal Gerrity","Cecile Brumley","Susanna Clausing","Barbar Taranto","Honey Hentges","Daisy Rapp","Irmgard Rotunno","Coletta Bothe","Genevive Phillips","Reynaldo Cameron","Francisca Digby","Sherrie Joachim","Camelia Williamson","Chandra Rojas","Heidi Agtarap","Marlena Martir","Enid Issac","Chelsey Paulino","Latasha Hostetter","Warren Humphries","Maegan Eatmon","Gertrud Heyward","Emmie Metts","Rossie Neugebauer","Barb Haberle","Kortney Leamon","Alene Peele","Jaqueline Elizondo","Giovanni Moose","Cathryn Ake","Tammi Scott","Elsa Capote","Craig Hoadley","Ferdinand Sweatman","Janeen Iwamoto","Harvey Gullick","Deena Eickhoff","Wava Zook","Karla Seats","Maureen Arocha","Myra Kirkpatrick","Gertrude Causby","Marya Fonville","Amparo Loveland","Janeth Hylton","Denis Dickinson","Charise Grunden","Colby Meshell","Clemente Griese","Genevieve Dougal","Jenell Tieu","Loree Monte","Sally Batton","Salvatore Teague","Lyla Cassinelli","Leighann Lamberti","Myrl Delaughter","Tracie Alverez","Raymonde Lundstrom","Everette Woodham","Rickey Flanary","See Crete","Young Monsour","Maya Osterman","Darrell Feiler","Christia Oatman","Mimi Forbes","Stanton Ackerley","Ines Farquharson","Geraldo Santee","Estella Charron","Kara Tift","Rodger Giambrone","Jospeh Remick","Corrine Gaumer","Mao Meggs","Luisa Jaimes","Kena Linsey","Cherilyn Vrooman","Tamra Dexter","Rossie Sweatt","Reuben Gabaldon","Taunya Pulice","Enriqueta Sieber","Sarah Ochoa","Vannessa Knickerbocker","Renea Canaday","Bailey Witmer","Thanh Nobile","Cristobal Ma","Colby Alanis","Abe Housand","Russ Whitmore","Sharolyn Cabrales","Fletcher Reynold","Denna Siefert","Mary Gaynor","Latoyia Romero","Barbra Chalk","Ted Frisk","Lore Guardiola","Summer Sine","Hisako Petrella","Jaime Despain","Coreen Greenstein","Joelle Massi","Rosia Hintz","Loralee Moone","Ciera Wheelwright","Tosha Mcduffee","Christina Mcglynn","Abdul Colgan","Charlott Packer","Trinh Haecker","Armandina Kimbro","Houston Justiniano","Perla Erazo","Elinore Mera","Dona Loveland","Deanne Holte","Grazyna Ravenell","Jenell Furry","Alexander Jeanpierre","Nobuko Ange","Sanjuana Mcdougle","Minnie Shankle","Earlene Wescott","Lucila Mcnally","Chanell Windham","Kathyrn Puff","Felicitas Lisenby","Hanna Dahms","Markita Reisner","Soledad Cron","Corey Haslem","Augustina Chien","Silvia Capuano","Kate Stice","Leisa Whitfield","Kimberlee Tartaglia","Janelle Breese","Suzi Ellard","Buffy Deschaine","Glendora Reuss","Kiera Sisk","Alina Neace","Suk See","Graham Ballard"],"stubs":["aaren","aarika","abagael","abagail","abbe","abbey","abbi","abbie","abby","abbye","abigael","abigail","abigale","abra","ada","adah","adaline","adan","adara","adda","addi","addia","addie","addy","adel","adela","adelaida","adelaide","adele","adelheid","adelice","adelina","adelind","adeline","adella","adelle","adena","adey","adi","adiana","adina","adora","adore","adoree","adorne","adrea","adria","adriaens","adrian","adriana","adriane","adrianna","adrianne","adriena","adrienne","aeriel","aeriela","aeriell","afton","ag","agace","agata","agatha","agathe","aggi","aggie","aggy","agna","agnella","agnes","agnese","agnesse","agneta","agnola","agretha","aida","aidan","aigneis","aila","aile","ailee","aileen","ailene","ailey","aili","ailina","ailis","ailsun","ailyn","aime","aimee","aimil","aindrea","ainslee","ainsley","ainslie","ajay","alaine","alameda","alana","alanah","alane","alanna","alayne","alberta","albertina","albertine","albina","alecia","aleda","aleece","aleen","alejandra","alejandrina","alena","alene","alessandra","aleta","alethea","alex","alexa","alexandra","alexandrina","alexi","alexia","alexina","alexine","alexis","alfi","alfie","alfreda","alfy","ali","alia","alica","alice","alicea","alicia","alida","alidia","alie","alika","alikee","alina","aline","alis","alisa","alisha","alison","alissa","alisun","alix","aliza","alla","alleen","allegra","allene","alli","allianora","allie","allina","allis","allison","allissa","allix","allsun","allx","ally","allyce","allyn","allys","allyson","alma","almeda","almeria","almeta","almira","almire","aloise","aloisia","aloysia","alta","althea","alvera","alverta","alvina","alvinia","alvira","alyce","alyda","alys","alysa","alyse","alysia","alyson","alyss","alyssa","amabel","amabelle","amalea","amalee","amaleta","amalia","amalie","amalita","amalle","amanda","amandi","amandie","amandy","amara","amargo","amata","amber","amberly","ambur","ame","amelia","amelie","amelina","ameline","amelita","ami","amie","amii","amil","amitie","amity","ammamaria","amy","amye","ana","anabal","anabel","anabella","anabelle","analiese","analise","anallese","anallise","anastasia","anastasie","anastassia","anatola","andee","andeee","anderea","andi","andie","andra","andrea","andreana","andree","andrei","andria","andriana","andriette","andromache","andy","anestassia","anet","anett","anetta","anette","ange","angel","angela","angele","angelia","angelica","angelika","angelina","angeline","angelique","angelita","angelle","angie","angil","angy","ania","anica","anissa","anita","anitra","anjanette","anjela","ann","ann-marie","anna","anna-diana","anna-diane","anna-maria","annabal","annabel","annabela","annabell","annabella","annabelle","annadiana","annadiane","annalee","annaliese","annalise","annamaria","annamarie","anne","anne-corinne","anne-marie","annecorinne","anneliese","annelise","annemarie","annetta","annette","anni","annice","annie","annis","annissa","annmaria","annmarie","annnora","annora","anny","anselma","ansley","anstice","anthe","anthea","anthia","anthiathia","antoinette","antonella","antonetta","antonia","antonie","antonietta","antonina","anya","appolonia","april","aprilette","ara","arabel","arabela","arabele","arabella","arabelle","arda","ardath","ardeen","ardelia","ardelis","ardella","ardelle","arden","ardene","ardenia","ardine","ardis","ardisj","ardith","ardra","ardyce","ardys","ardyth","aretha","ariadne","ariana","aridatha","ariel","ariela","ariella","arielle","arlana","arlee","arleen","arlen","arlena","arlene","arleta","arlette","arleyne","arlie","arliene","arlina","arlinda","arline","arluene","arly","arlyn","arlyne","aryn","ashely","ashia","ashien","ashil","ashla","ashlan","ashlee","ashleigh","ashlen","ashley","ashli","ashlie","ashly","asia","astra","astrid","astrix","atalanta","athena","athene","atlanta","atlante","auberta","aubine","aubree","aubrette","aubrey","aubrie","aubry","audi","audie","audra","audre","audrey","audrie","audry","audrye","audy","augusta","auguste","augustina","augustine","aundrea","aura","aurea","aurel","aurelea","aurelia","aurelie","auria","aurie","aurilia","aurlie","auroora","aurora","aurore","austin","austina","austine","ava","aveline","averil","averyl","avie","avis","aviva","avivah","avril","avrit","ayn","bab","babara","babb","babbette","babbie","babette","babita","babs","bambi","bambie","bamby","barb","barbabra","barbara","barbara-anne","barbaraanne","barbe","barbee","barbette","barbey","barbi","barbie","barbra","barby","bari","barrie","barry","basia","bathsheba","batsheva","bea","beatrice","beatrisa","beatrix","beatriz","bebe","becca","becka","becki","beckie","becky","bee","beilul","beitris","bekki","bel","belia","belicia","belinda","belita","bell","bella","bellanca","belle","bellina","belva","belvia","bendite","benedetta","benedicta","benedikta","benetta","benita","benni","bennie","benny","benoite","berenice","beret","berget","berna","bernadene","bernadette","bernadina","bernadine","bernardina","bernardine","bernelle","bernete","bernetta","bernette","berni","bernice","bernie","bernita","berny","berri","berrie","berry","bert","berta","berte","bertha","berthe","berti","bertie","bertina","bertine","berty","beryl","beryle","bess","bessie","bessy","beth","bethanne","bethany","bethena","bethina","betsey","betsy","betta","bette","bette-ann","betteann","betteanne","betti","bettina","bettine","betty","bettye","beulah","bev","beverie","beverlee","beverley","beverlie","beverly","bevvy","bianca","bianka","bibbie","bibby","bibbye","bibi","biddie","biddy","bidget","bili","bill","billi","billie","billy","billye","binni","binnie","binny","bird","birdie","birgit","birgitta","blair","blaire","blake","blakelee","blakeley","blanca","blanch","blancha","blanche","blinni","blinnie","blinny","bliss","blisse","blithe","blondell","blondelle","blondie","blondy","blythe","bobbe","bobbee","bobbette","bobbi","bobbie","bobby","bobbye","bobette","bobina","bobine","bobinette","bonita","bonnee","bonni","bonnibelle","bonnie","bonny","brana","brandais","brande","brandea","brandi","brandice","brandie","brandise","brandy","breanne","brear","bree","breena","bren","brena","brenda","brenn","brenna","brett","bria","briana","brianna","brianne","bride","bridget","bridgette","bridie","brier","brietta","brigid","brigida","brigit","brigitta","brigitte","brina","briney","brinn","brinna","briny","brit","brita","britney","britni","britt","britta","brittan","brittaney","brittani","brittany","britte","britteny","brittne","brittney","brittni","brook","brooke","brooks","brunhilda","brunhilde","bryana","bryn","bryna","brynn","brynna","brynne","buffy","bunni","bunnie","bunny","cacilia","cacilie","cahra","cairistiona","caitlin","caitrin","cal","calida","calla","calley","calli","callida","callie","cally","calypso","cam","camala","camel","camella","camellia","cami","camila","camile","camilla","camille","cammi","cammie","cammy","candace","candi","candice","candida","candide","candie","candis","candra","candy","caprice","cara","caralie","caren","carena","caresa","caressa","caresse","carey","cari","caria","carie","caril","carilyn","carin","carina","carine","cariotta","carissa","carita","caritta","carla","carlee","carleen","carlen","carlene","carley","carlie","carlin","carlina","carline","carlita","carlota","carlotta","carly","carlye","carlyn","carlynn","carlynne","carma","carmel","carmela","carmelia","carmelina","carmelita","carmella","carmelle","carmen","carmencita","carmina","carmine","carmita","carmon","caro","carol","carol-jean","carola","carolan","carolann","carole","carolee","carolin","carolina","caroline","caroljean","carolyn","carolyne","carolynn","caron","carree","carri","carrie","carrissa","carroll","carry","cary","caryl","caryn","casandra","casey","casi","casie","cass","cassandra","cassandre","cassandry","cassaundra","cassey","cassi","cassie","cassondra","cassy","catarina","cate","caterina","catha","catharina","catharine","cathe","cathee","catherin","catherina","catherine","cathi","cathie","cathleen","cathlene","cathrin","cathrine","cathryn","cathy","cathyleen","cati","catie","catina","catlaina","catlee","catlin","catrina","catriona","caty","caye","cayla","cecelia","cecil","cecile","ceciley","cecilia","cecilla","cecily","ceil","cele","celene","celesta","celeste","celestia","celestina","celestine","celestyn","celestyna","celia","celie","celina","celinda","celine","celinka","celisse","celka","celle","cesya","chad","chanda","chandal","chandra","channa","chantal","chantalle","charil","charin","charis","charissa","charisse","charita","charity","charla","charlean","charleen","charlena","charlene","charline","charlot","charlotta","charlotte","charmain","charmaine","charmane","charmian","charmine","charmion","charo","charyl","chastity","chelsae","chelsea","chelsey","chelsie","chelsy","cher","chere","cherey","cheri","cherianne","cherice","cherida","cherie","cherilyn","cherilynn","cherin","cherise","cherish","cherlyn","cherri","cherrita","cherry","chery","cherye","cheryl","cheslie","chiarra","chickie","chicky","chiquia","chiquita","chlo","chloe","chloette","chloris","chris","chrissie","chrissy","christa","christabel","christabella","christal","christalle","christan","christean","christel","christen","christi","christian","christiana","christiane","christie","christin","christina","christine","christy","christye","christyna","chrysa","chrysler","chrystal","chryste","chrystel","cicely","cicily","ciel","cilka","cinda","cindee","cindelyn","cinderella","cindi","cindie","cindra","cindy","cinnamon","cissiee","cissy","clair","claire","clara","clarabelle","clare","claresta","clareta","claretta","clarette","clarey","clari","claribel","clarice","clarie","clarinda","clarine","clarissa","clarisse","clarita","clary","claude","claudelle","claudetta","claudette","claudia","claudie","claudina","claudine","clea","clem","clemence","clementia","clementina","clementine","clemmie","clemmy","cleo","cleopatra","clerissa","clio","clo","cloe","cloris","clotilda","clovis","codee","codi","codie","cody","coleen","colene","coletta","colette","colleen","collen","collete","collette","collie","colline","colly","con","concettina","conchita","concordia","conni","connie","conny","consolata","constance","constancia","constancy","constanta","constantia","constantina","constantine","consuela","consuelo","cookie","cora","corabel","corabella","corabelle","coral","coralie","coraline","coralyn","cordelia","cordelie","cordey","cordi","cordie","cordula","cordy","coreen","corella","corenda","corene","coretta","corette","corey","cori","corie","corilla","corina","corine","corinna","corinne","coriss","corissa","corliss","corly","cornela","cornelia","cornelle","cornie","corny","correna","correy","corri","corrianne","corrie","corrina","corrine","corrinne","corry","cortney","cory","cosetta","cosette","costanza","courtenay","courtnay","courtney","crin","cris","crissie","crissy","crista","cristabel","cristal","cristen","cristi","cristie","cristin","cristina","cristine","cristionna","cristy","crysta","crystal","crystie","cthrine","cyb","cybil","cybill","cymbre","cynde","cyndi","cyndia","cyndie","cyndy","cynthea","cynthia","cynthie","cynthy","dacey","dacia","dacie","dacy","dael","daffi","daffie","daffy","dagmar","dahlia","daile","daisey","daisi","daisie","daisy","dale","dalenna","dalia","dalila","dallas","daloris","damara","damaris","damita","dana","danell","danella","danette","dani","dania","danica","danice","daniela","daniele","daniella","danielle","danika","danila","danit","danita","danna","danni","dannie","danny","dannye","danya","danyelle","danyette","daphene","daphna","daphne","dara","darb","darbie","darby","darcee","darcey","darci","darcie","darcy","darda","dareen","darell","darelle","dari","daria","darice","darla","darleen","darlene","darline","darlleen","daron","darrelle","darryl","darsey","darsie","darya","daryl","daryn","dasha","dasi","dasie","dasya","datha","daune","daveen","daveta","davida","davina","davine","davita","dawn","dawna","dayle","dayna","ddene","de","deana","deane","deanna","deanne","deb","debbi","debbie","debby","debee","debera","debi","debor","debora","deborah","debra","dede","dedie","dedra","dee","dee dee","deeann","deeanne","deedee","deena","deerdre","deeyn","dehlia","deidre","deina","deirdre","del","dela","delcina","delcine","delia","delila","delilah","delinda","dell","della","delly","delora","delores","deloria","deloris","delphine","delphinia","demeter","demetra","demetria","demetris","dena","deni","denice","denise","denna","denni","dennie","denny","deny","denys","denyse","deonne","desdemona","desirae","desiree","desiri","deva","devan","devi","devin","devina","devinne","devon","devondra","devonna","devonne","devora","di","diahann","dian","diana","diandra","diane","diane-marie","dianemarie","diann","dianna","dianne","diannne","didi","dido","diena","dierdre","dina","dinah","dinnie","dinny","dion","dione","dionis","dionne","dita","dix","dixie","dniren","dode","dodi","dodie","dody","doe","doll","dolley","dolli","dollie","dolly","dolores","dolorita","doloritas","domeniga","dominga","domini","dominica","dominique","dona","donella","donelle","donetta","donia","donica","donielle","donna","donnamarie","donni","donnie","donny","dora","doralia","doralin","doralyn","doralynn","doralynne","dore","doreen","dorelia","dorella","dorelle","dorena","dorene","doretta","dorette","dorey","dori","doria","dorian","dorice","dorie","dorine","doris","dorisa","dorise","dorita","doro","dorolice","dorolisa","dorotea","doroteya","dorothea","dorothee","dorothy","dorree","dorri","dorrie","dorris","dorry","dorthea","dorthy","dory","dosi","dot","doti","dotti","dottie","dotty","dre","dreddy","dredi","drona","dru","druci","drucie","drucill","drucy","drusi","drusie","drusilla","drusy","dulce","dulcea","dulci","dulcia","dulciana","dulcie","dulcine","dulcinea","dulcy","dulsea","dusty","dyan","dyana","dyane","dyann","dyanna","dyanne","dyna","dynah","eachelle","eada","eadie","eadith","ealasaid","eartha","easter","eba","ebba","ebonee","ebony","eda","eddi","eddie","eddy","ede","edee","edeline","eden","edi","edie","edin","edita","edith","editha","edithe","ediva","edna","edwina","edy","edyth","edythe","effie","eileen","eilis","eimile","eirena","ekaterina","elaina","elaine","elana","elane","elayne","elberta","elbertina","elbertine","eleanor","eleanora","eleanore","electra","eleen","elena","elene","eleni","elenore","eleonora","eleonore","elfie","elfreda","elfrida","elfrieda","elga","elianora","elianore","elicia","elie","elinor","elinore","elisa","elisabet","elisabeth","elisabetta","elise","elisha","elissa","elita","eliza","elizabet","elizabeth","elka","elke","ella","elladine","elle","ellen","ellene","ellette","elli","ellie","ellissa","elly","ellyn","ellynn","elmira","elna","elnora","elnore","eloisa","eloise","elonore","elora","elsa","elsbeth","else","elset","elsey","elsi","elsie","elsinore","elspeth","elsy","elva","elvera","elvina","elvira","elwira","elyn","elyse","elysee","elysha","elysia","elyssa","em","ema","emalee","emalia","emelda","emelia","emelina","emeline","emelita","emelyne","emera","emilee","emili","emilia","emilie","emiline","emily","emlyn","emlynn","emlynne","emma","emmalee","emmaline","emmalyn","emmalynn","emmalynne","emmeline","emmey","emmi","emmie","emmy","emmye","emogene","emyle","emylee","engracia","enid","enrica","enrichetta","enrika","enriqueta","eolanda","eolande","eran","erda","erena","erica","ericha","ericka","erika","erin","erina","erinn","erinna","erma","ermengarde","ermentrude","ermina","erminia","erminie","erna","ernaline","ernesta","ernestine","ertha","eryn","esma","esmaria","esme","esmeralda","essa","essie","essy","esta","estel","estele","estell","estella","estelle","ester","esther","estrella","estrellita","ethel","ethelda","ethelin","ethelind","etheline","ethelyn","ethyl","etta","etti","ettie","etty","eudora","eugenia","eugenie","eugine","eula","eulalie","eunice","euphemia","eustacia","eva","evaleen","evangelia","evangelin","evangelina","evangeline","evania","evanne","eve","eveleen","evelina","eveline","evelyn","evey","evie","evita","evonne","evvie","evvy","evy","eyde","eydie","ezmeralda","fae","faina","faith","fallon","fan","fanchette","fanchon","fancie","fancy","fanechka","fania","fanni","fannie","fanny","fanya","fara","farah","farand","farica","farra","farrah","farrand","faun","faunie","faustina","faustine","fawn","fawne","fawnia","fay","faydra","faye","fayette","fayina","fayre","fayth","faythe","federica","fedora","felecia","felicdad","felice","felicia","felicity","felicle","felipa","felisha","felita","feliza","fenelia","feodora","ferdinanda","ferdinande","fern","fernanda","fernande","fernandina","ferne","fey","fiann","fianna","fidela","fidelia","fidelity","fifi","fifine","filia","filide","filippa","fina","fiona","fionna","fionnula","fiorenze","fleur","fleurette","flo","flor","flora","florance","flore","florella","florence","florencia","florentia","florenza","florette","flori","floria","florida","florie","florina","florinda","floris","florri","florrie","florry","flory","flossi","flossie","flossy","flss","fran","francene","frances","francesca","francine","francisca","franciska","francoise","francyne","frank","frankie","franky","franni","frannie","franny","frayda","fred","freda","freddi","freddie","freddy","fredelia","frederica","fredericka","frederique","fredi","fredia","fredra","fredrika","freida","frieda","friederike","fulvia","gabbey","gabbi","gabbie","gabey","gabi","gabie","gabriel","gabriela","gabriell","gabriella","gabrielle","gabriellia","gabrila","gaby","gae","gael","gail","gale","galina","garland","garnet","garnette","gates","gavra","gavrielle","gay","gaye","gayel","gayla","gayle","gayleen","gaylene","gaynor","gelya","gena","gene","geneva","genevieve","genevra","genia","genna","genni","gennie","gennifer","genny","genovera","genvieve","george","georgeanna","georgeanne","georgena","georgeta","georgetta","georgette","georgia","georgiana","georgianna","georgianne","georgie","georgina","georgine","geralda","geraldine","gerda","gerhardine","geri","gerianna","gerianne","gerladina","germain","germaine","germana","gerri","gerrie","gerrilee","gerry","gert","gerta","gerti","gertie","gertrud","gertruda","gertrude","gertrudis","gerty","giacinta","giana","gianina","gianna","gigi","gilberta","gilberte","gilbertina","gilbertine","gilda","gilemette","gill","gillan","gilli","gillian","gillie","gilligan","gilly","gina","ginelle","ginevra","ginger","ginni","ginnie","ginnifer","ginny","giorgia","giovanna","gipsy","giralda","gisela","gisele","gisella","giselle","giuditta","giulia","giulietta","giustina","gizela","glad","gladi","gladys","gleda","glen","glenda","glenine","glenn","glenna","glennie","glennis","glori","gloria","gloriana","gloriane","glory","glyn","glynda","glynis","glynnis","gnni","godiva","golda","goldarina","goldi","goldia","goldie","goldina","goldy","grace","gracia","gracie","grata","gratia","gratiana","gray","grayce","grazia","greer","greta","gretal","gretchen","grete","gretel","grethel","gretna","gretta","grier","griselda","grissel","guendolen","guenevere","guenna","guglielma","gui","guillema","guillemette","guinevere","guinna","gunilla","gus","gusella","gussi","gussie","gussy","gusta","gusti","gustie","gusty","gwen","gwendolen","gwendolin","gwendolyn","gweneth","gwenette","gwenneth","gwenni","gwennie","gwenny","gwenora","gwenore","gwyn","gwyneth","gwynne","gypsy","hadria","hailee","haily","haleigh","halette","haley","hali","halie","halimeda","halley","halli","hallie","hally","hana","hanna","hannah","hanni","hannie","hannis","hanny","happy","harlene","harley","harli","harlie","harmonia","harmonie","harmony","harri","harrie","harriet","harriett","harrietta","harriette","harriot","harriott","hatti","hattie","hatty","hayley","hazel","heath","heather","heda","hedda","heddi","heddie","hedi","hedvig","hedvige","hedwig","hedwiga","hedy","heida","heidi","heidie","helaina","helaine","helen","helen-elizabeth","helena","helene","helenka","helga","helge","helli","heloise","helsa","helyn","hendrika","henka","henrie","henrieta","henrietta","henriette","henryetta","hephzibah","hermia","hermina","hermine","herminia","hermione","herta","hertha","hester","hesther","hestia","hetti","hettie","hetty","hilary","hilda","hildagard","hildagarde","hilde","hildegaard","hildegarde","hildy","hillary","hilliary","hinda","holli","hollie","holly","holly-anne","hollyanne","honey","honor","honoria","hope","horatia","hortense","hortensia","hulda","hyacinth","hyacintha","hyacinthe","hyacinthia","hyacinthie","hynda","ianthe","ibbie","ibby","ida","idalia","idalina","idaline","idell","idelle","idette","ileana","ileane","ilene","ilise","ilka","illa","ilsa","ilse","ilysa","ilyse","ilyssa","imelda","imogen","imogene","imojean","ina","indira","ines","inesita","inessa","inez","inga","ingaberg","ingaborg","inge","ingeberg","ingeborg","inger","ingrid","ingunna","inna","iolande","iolanthe","iona","iormina","ira","irena","irene","irina","iris","irita","irma","isa","isabel","isabelita","isabella","isabelle","isadora","isahella","iseabal","isidora","isis","isobel","issi","issie","issy","ivett","ivette","ivie","ivonne","ivory","ivy","izabel","jacenta","jacinda","jacinta","jacintha","jacinthe","jackelyn","jacki","jackie","jacklin","jacklyn","jackquelin","jackqueline","jacky","jaclin","jaclyn","jacquelin","jacqueline","jacquelyn","jacquelynn","jacquenetta","jacquenette","jacquetta","jacquette","jacqui","jacquie","jacynth","jada","jade","jaime","jaimie","jaine","jami","jamie","jamima","jammie","jan","jana","janaya","janaye","jandy","jane","janean","janeczka","janeen","janel","janela","janella","janelle","janene","janenna","janessa","janet","janeta","janetta","janette","janeva","janey","jania","janice","janie","janifer","janina","janine","janis","janith","janka","janna","jannel","jannelle","janot","jany","jaquelin","jaquelyn","jaquenetta","jaquenette","jaquith","jasmin","jasmina","jasmine","jayme","jaymee","jayne","jaynell","jazmin","jean","jeana","jeane","jeanelle","jeanette","jeanie","jeanine","jeanna","jeanne","jeannette","jeannie","jeannine","jehanna","jelene","jemie","jemima","jemimah","jemmie","jemmy","jen","jena","jenda","jenelle","jeni","jenica","jeniece","jenifer","jeniffer","jenilee","jenine","jenn","jenna","jennee","jennette","jenni","jennica","jennie","jennifer","jennilee","jennine","jenny","jeralee","jere","jeri","jermaine","jerrie","jerrilee","jerrilyn","jerrine","jerry","jerrylee","jess","jessa","jessalin","jessalyn","jessamine","jessamyn","jesse","jesselyn","jessi","jessica","jessie","jessika","jessy","jewel","jewell","jewelle","jill","jillana","jillane","jillayne","jilleen","jillene","jilli","jillian","jillie","jilly","jinny","jo","jo ann","jo-ann","jo-anne","joan","joana","joane","joanie","joann","joanna","joanne","joannes","jobey","jobi","jobie","jobina","joby","jobye","jobyna","jocelin","joceline","jocelyn","jocelyne","jodee","jodi","jodie","jody","joeann","joela","joelie","joell","joella","joelle","joellen","joelly","joellyn","joelynn","joete","joey","johanna","johannah","johna","johnath","johnette","johnna","joice","jojo","jolee","joleen","jolene","joletta","joli","jolie","joline","joly","jolyn","jolynn","jonell","joni","jonie","jonis","jordain","jordan","jordana","jordanna","jorey","jori","jorie","jorrie","jorry","joscelin","josee","josefa","josefina","josepha","josephina","josephine","josey","josi","josie","josselyn","josy","jourdan","joy","joya","joyan","joyann","joyce","joycelin","joye","jsandye","juana","juanita","judi","judie","judith","juditha","judy","judye","juieta","julee","juli","julia","juliana","juliane","juliann","julianna","julianne","julie","julienne","juliet","julieta","julietta","juliette","julina","juline","julissa","julita","june","junette","junia","junie","junina","justina","justine","justinn","jyoti","kacey","kacie","kacy","kaela","kai","kaia","kaila","kaile","kailey","kaitlin","kaitlyn","kaitlynn","kaja","kakalina","kala","kaleena","kali","kalie","kalila","kalina","kalinda","kalindi","kalli","kally","kameko","kamila","kamilah","kamillah","kandace","kandy","kania","kanya","kara","kara-lynn","karalee","karalynn","kare","karee","karel","karen","karena","kari","karia","karie","karil","karilynn","karin","karina","karine","kariotta","karisa","karissa","karita","karla","karlee","karleen","karlen","karlene","karlie","karlotta","karlotte","karly","karlyn","karmen","karna","karol","karola","karole","karolina","karoline","karoly","karon","karrah","karrie","karry","kary","karyl","karylin","karyn","kasey","kass","kassandra","kassey","kassi","kassia","kassie","kat","kata","katalin","kate","katee","katerina","katerine","katey","kath","katha","katharina","katharine","katharyn","kathe","katherina","katherine","katheryn","kathi","kathie","kathleen","kathlin","kathrine","kathryn","kathryne","kathy","kathye","kati","katie","katina","katine","katinka","katleen","katlin","katrina","katrine","katrinka","katti","kattie","katuscha","katusha","katy","katya","kay","kaycee","kaye","kayla","kayle","kaylee","kayley","kaylil","kaylyn","keeley","keelia","keely","kelcey","kelci","kelcie","kelcy","kelila","kellen","kelley","kelli","kellia","kellie","kellina","kellsie","kelly","kellyann","kelsey","kelsi","kelsy","kendra","kendre","kenna","keri","keriann","kerianne","kerri","kerrie","kerrill","kerrin","kerry","kerstin","kesley","keslie","kessia","kessiah","ketti","kettie","ketty","kevina","kevyn","ki","kiah","kial","kiele","kiersten","kikelia","kiley","kim","kimberlee","kimberley","kimberli","kimberly","kimberlyn","kimbra","kimmi","kimmie","kimmy","kinna","kip","kipp","kippie","kippy","kira","kirbee","kirbie","kirby","kiri","kirsten","kirsteni","kirsti","kirstin","kirstyn","kissee","kissiah","kissie","kit","kitti","kittie","kitty","kizzee","kizzie","klara","klarika","klarrisa","konstance","konstanze","koo","kora","koral","koralle","kordula","kore","korella","koren","koressa","kori","korie","korney","korrie","korry","kris","krissie","krissy","krista","kristal","kristan","kriste","kristel","kristen","kristi","kristien","kristin","kristina","kristine","kristy","kristyn","krysta","krystal","krystalle","krystle","krystyna","kyla","kyle","kylen","kylie","kylila","kylynn","kym","kynthia","kyrstin","la verne","lacee","lacey","lacie","lacy","ladonna","laetitia","laina","lainey","lana","lanae","lane","lanette","laney","lani","lanie","lanita","lanna","lanni","lanny","lara","laraine","lari","larina","larine","larisa","larissa","lark","laryssa","latashia","latia","latisha","latrena","latrina","laura","lauraine","laural","lauralee","laure","lauree","laureen","laurel","laurella","lauren","laurena","laurene","lauretta","laurette","lauri","laurianne","laurice","laurie","lauryn","lavena","laverna","laverne","lavina","lavinia","lavinie","layla","layne","layney","lea","leah","leandra","leann","leanna","leanor","leanora","lebbie","leda","lee","leeann","leeanne","leela","leelah","leena","leesa","leese","legra","leia","leigh","leigha","leila","leilah","leisha","lela","lelah","leland","lelia","lena","lenee","lenette","lenka","lenna","lenora","lenore","leodora","leoine","leola","leoline","leona","leonanie","leone","leonelle","leonie","leonora","leonore","leontine","leontyne","leora","leshia","lesley","lesli","leslie","lesly","lesya","leta","lethia","leticia","letisha","letitia","letizia","letta","letti","lettie","letty","lexi","lexie","lexine","lexis","lexy","leyla","lezlie","lia","lian","liana","liane","lianna","lianne","lib","libbey","libbi","libbie","libby","licha","lida","lidia","liesa","lil","lila","lilah","lilas","lilia","lilian","liliane","lilias","lilith","lilla","lilli","lillian","lillis","lilllie","lilly","lily","lilyan","lin","lina","lind","linda","lindi","lindie","lindsay","lindsey","lindsy","lindy","linea","linell","linet","linette","linn","linnea","linnell","linnet","linnie","linzy","lira","lisa","lisabeth","lisbeth","lise","lisetta","lisette","lisha","lishe","lissa","lissi","lissie","lissy","lita","liuka","liv","liva","livia","livvie","livvy","livvyy","livy","liz","liza","lizabeth","lizbeth","lizette","lizzie","lizzy","loella","lois","loise","lola","loleta","lolita","lolly","lona","lonee","loni","lonna","lonni","lonnie","lora","lorain","loraine","loralee","loralie","loralyn","loree","loreen","lorelei","lorelle","loren","lorena","lorene","lorenza","loretta","lorette","lori","loria","lorianna","lorianne","lorie","lorilee","lorilyn","lorinda","lorine","lorita","lorna","lorne","lorraine","lorrayne","lorri","lorrie","lorrin","lorry","lory","lotta","lotte","lotti","lottie","lotty","lou","louella","louisa","louise","louisette","loutitia","lu","luce","luci","lucia","luciana","lucie","lucienne","lucila","lucilia","lucille","lucina","lucinda","lucine","lucita","lucky","lucretia","lucy","ludovika","luella","luelle","luisa","luise","lula","lulita","lulu","lura","lurette","lurleen","lurlene","lurline","lusa","luz","lyda","lydia","lydie","lyn","lynda","lynde","lyndel","lyndell","lyndsay","lyndsey","lyndsie","lyndy","lynea","lynelle","lynett","lynette","lynn","lynna","lynne","lynnea","lynnell","lynnelle","lynnet","lynnett","lynnette","lynsey","lyssa","mab","mabel","mabelle","mable","mada","madalena","madalyn","maddalena","maddi","maddie","maddy","madel","madelaine","madeleine","madelena","madelene","madelin","madelina","madeline","madella","madelle","madelon","madelyn","madge","madlen","madlin","madonna","mady","mae","maegan","mag","magda","magdaia","magdalen","magdalena","magdalene","maggee","maggi","maggie","maggy","mahala","mahalia","maia","maible","maiga","maighdiln","mair","maire","maisey","maisie","maitilde","mala","malanie","malena","malia","malina","malinda","malinde","malissa","malissia","mallissa","mallorie","mallory","malorie","malory","malva","malvina","malynda","mame","mamie","manda","mandi","mandie","mandy","manon","manya","mara","marabel","marcela","marcelia","marcella","marcelle","marcellina","marcelline","marchelle","marci","marcia","marcie","marcile","marcille","marcy","mareah","maren","marena","maressa","marga","margalit","margalo","margaret","margareta","margarete","margaretha","margarethe","margaretta","margarette","margarita","margaux","marge","margeaux","margery","marget","margette","margi","margie","margit","margo","margot","margret","marguerite","margy","mari","maria","mariam","marian","mariana","mariann","marianna","marianne","maribel","maribelle","maribeth","marice","maridel","marie","marie-ann","marie-jeanne","marieann","mariejeanne","mariel","mariele","marielle","mariellen","marietta","mariette","marigold","marijo","marika","marilee","marilin","marillin","marilyn","marin","marina","marinna","marion","mariquilla","maris","marisa","mariska","marissa","marita","maritsa","mariya","marj","marja","marje","marji","marjie","marjorie","marjory","marjy","marketa","marla","marlane","marleah","marlee","marleen","marlena","marlene","marley","marlie","marline","marlo","marlyn","marna","marne","marney","marni","marnia","marnie","marquita","marrilee","marris","marrissa","marsha","marsiella","marta","martelle","martguerita","martha","marthe","marthena","marti","martica","martie","martina","martita","marty","martynne","mary","marya","maryann","maryanna","maryanne","marybelle","marybeth","maryellen","maryjane","maryjo","maryl","marylee","marylin","marylinda","marylou","marylynne","maryrose","marys","marysa","masha","matelda","mathilda","mathilde","matilda","matilde","matti","mattie","matty","maud","maude","maudie","maura","maure","maureen","maureene","maurene","maurine","maurise","maurita","maurizia","mavis","mavra","max","maxi","maxie","maxine","maxy","may","maybelle","maye","mead","meade","meagan","meaghan","meara","mechelle","meg","megan","megen","meggi","meggie","meggy","meghan","meghann","mehetabel","mei","mel","mela","melamie","melania","melanie","melantha","melany","melba","melesa","melessa","melicent","melina","melinda","melinde","melisa","melisande","melisandra","melisenda","melisent","melissa","melisse","melita","melitta","mella","melli","mellicent","mellie","mellisa","mellisent","melloney","melly","melodee","melodie","melody","melonie","melony","melosa","melva","mercedes","merci","mercie","mercy","meredith","meredithe","meridel","meridith","meriel","merilee","merilyn","meris","merissa","merl","merla","merle","merlina","merline","merna","merola","merralee","merridie","merrie","merrielle","merrile","merrilee","merrili","merrill","merrily","merry","mersey","meryl","meta","mia","micaela","michaela","michaelina","michaeline","michaella","michal","michel","michele","michelina","micheline","michell","michelle","micki","mickie","micky","midge","mignon","mignonne","miguela","miguelita","mikaela","mil","mildred","mildrid","milena","milicent","milissent","milka","milli","millicent","millie","millisent","milly","milzie","mimi","min","mina","minda","mindy","minerva","minetta","minette","minna","minnaminnie","minne","minni","minnie","minnnie","minny","minta","miof mela","miquela","mira","mirabel","mirabella","mirabelle","miran","miranda","mireielle","mireille","mirella","mirelle","miriam","mirilla","mirna","misha","missie","missy","misti","misty","mitzi","modesta","modestia","modestine","modesty","moina","moira","moll","mollee","molli","mollie","molly","mommy","mona","monah","monica","monika","monique","mora","moreen","morena","morgan","morgana","morganica","morganne","morgen","moria","morissa","morna","moselle","moyna","moyra","mozelle","muffin","mufi","mufinella","muire","mureil","murial","muriel","murielle","myra","myrah","myranda","myriam","myrilla","myrle","myrlene","myrna","myrta","myrtia","myrtice","myrtie","myrtle","nada","nadean","nadeen","nadia","nadine","nadiya","nady","nadya","nalani","nan","nana","nananne","nance","nancee","nancey","nanci","nancie","nancy","nanete","nanette","nani","nanice","nanine","nannette","nanni","nannie","nanny","nanon","naoma","naomi","nara","nari","nariko","nat","nata","natala","natalee","natalie","natalina","nataline","natalya","natasha","natassia","nathalia","nathalie","natividad","natka","natty","neala","neda","nedda","nedi","neely","neila","neile","neilla","neille","nelia","nelie","nell","nelle","nelli","nellie","nelly","nerissa","nerita","nert","nerta","nerte","nerti","nertie","nerty","nessa","nessi","nessie","nessy","nesta","netta","netti","nettie","nettle","netty","nevsa","neysa","nichol","nichole","nicholle","nicki","nickie","nicky","nicol","nicola","nicole","nicolea","nicolette","nicoli","nicolina","nicoline","nicolle","nikaniki","nike","niki","nikki","nikkie","nikoletta","nikolia","nina","ninetta","ninette","ninnetta","ninnette","ninon","nissa","nisse","nissie","nissy","nita","nixie","noami","noel","noelani","noell","noella","noelle","noellyn","noelyn","noemi","nola","nolana","nolie","nollie","nomi","nona","nonah","noni","nonie","nonna","nonnah","nora","norah","norean","noreen","norene","norina","norine","norma","norri","norrie","norry","novelia","nydia","nyssa","octavia","odele","odelia","odelinda","odella","odelle","odessa","odetta","odette","odilia","odille","ofelia","ofella","ofilia","ola","olenka","olga","olia","olimpia","olive","olivette","olivia","olivie","oliy","ollie","olly","olva","olwen","olympe","olympia","olympie","ondrea","oneida","onida","oona","opal","opalina","opaline","ophelia","ophelie","ora","oralee","oralia","oralie","oralla","oralle","orel","orelee","orelia","orelie","orella","orelle","oriana","orly","orsa","orsola","ortensia","otha","othelia","othella","othilia","othilie","ottilie","page","paige","paloma","pam","pamela","pamelina","pamella","pammi","pammie","pammy","pandora","pansie","pansy","paola","paolina","papagena","pat","patience","patrica","patrice","patricia","patrizia","patsy","patti","pattie","patty","paula","paule","pauletta","paulette","pauli","paulie","paulina","pauline","paulita","pauly","pavia","pavla","pearl","pearla","pearle","pearline","peg","pegeen","peggi","peggie","peggy","pen","penelopa","penelope","penni","pennie","penny","pepi","pepita","peri","peria","perl","perla","perle","perri","perrine","perry","persis","pet","peta","petra","petrina","petronella","petronia","petronilla","petronille","petunia","phaedra","phaidra","phebe","phedra","phelia","phil","philipa","philippa","philippe","philippine","philis","phillida","phillie","phillis","philly","philomena","phoebe","phylis","phyllida","phyllis","phyllys","phylys","pia","pier","pierette","pierrette","pietra","piper","pippa","pippy","polly","pollyanna","pooh","poppy","portia","pris","prisca","priscella","priscilla","prissie","pru","prudence","prudi","prudy","prue","queenie","quentin","querida","quinn","quinta","quintana","quintilla","quintina","rachael","rachel","rachele","rachelle","rae","raeann","raf","rafa","rafaela","rafaelia","rafaelita","rahal","rahel","raina","raine","rakel","ralina","ramona","ramonda","rana","randa","randee","randene","randi","randie","randy","ranee","rani","rania","ranice","ranique","ranna","raphaela","raquel","raquela","rasia","rasla","raven","ray","raychel","raye","rayna","raynell","rayshell","rea","reba","rebbecca","rebe","rebeca","rebecca","rebecka","rebeka","rebekah","rebekkah","ree","reeba","reena","reeta","reeva","regan","reggi","reggie","regina","regine","reiko","reina","reine","remy","rena","renae","renata","renate","rene","renee","renell","renelle","renie","rennie","reta","retha","revkah","rey","reyna","rhea","rheba","rheta","rhetta","rhiamon","rhianna","rhianon","rhoda","rhodia","rhodie","rhody","rhona","rhonda","riane","riannon","rianon","rica","ricca","rici","ricki","rickie","ricky","riki","rikki","rina","risa","rita","riva","rivalee","rivi","rivkah","rivy","roana","roanna","roanne","robbi","robbie","robbin","robby","robbyn","robena","robenia","roberta","robin","robina","robinet","robinett","robinetta","robinette","robinia","roby","robyn","roch","rochell","rochella","rochelle","rochette","roda","rodi","rodie","rodina","rois","romola","romona","romonda","romy","rona","ronalda","ronda","ronica","ronna","ronni","ronnica","ronnie","ronny","roobbie","rora","rori","rorie","rory","ros","rosa","rosabel","rosabella","rosabelle","rosaleen","rosalia","rosalie","rosalind","rosalinda","rosalinde","rosaline","rosalyn","rosalynd","rosamond","rosamund","rosana","rosanna","rosanne","rose","roseann","roseanna","roseanne","roselia","roselin","roseline","rosella","roselle","rosemaria","rosemarie","rosemary","rosemonde","rosene","rosetta","rosette","roshelle","rosie","rosina","rosita","roslyn","rosmunda","rosy","row","rowe","rowena","roxana","roxane","roxanna","roxanne","roxi","roxie","roxine","roxy","roz","rozalie","rozalin","rozamond","rozanna","rozanne","roze","rozele","rozella","rozelle","rozina","rubetta","rubi","rubia","rubie","rubina","ruby","ruperta","ruth","ruthann","ruthanne","ruthe","ruthi","ruthie","ruthy","ryann","rycca","saba","sabina","sabine","sabra","sabrina","sacha","sada","sadella","sadie","sadye","saidee","sal","salaidh","sallee","salli","sallie","sally","sallyann","sallyanne","saloma","salome","salomi","sam","samantha","samara","samaria","sammy","sande","sandi","sandie","sandra","sandy","sandye","sapphira","sapphire","sara","sara-ann","saraann","sarah","sarajane","saree","sarena","sarene","sarette","sari","sarina","sarine","sarita","sascha","sasha","sashenka","saudra","saundra","savina","sayre","scarlet","scarlett","sean","seana","seka","sela","selena","selene","selestina","selia","selie","selina","selinda","seline","sella","selle","selma","sena","sephira","serena","serene","shae","shaina","shaine","shalna","shalne","shana","shanda","shandee","shandeigh","shandie","shandra","shandy","shane","shani","shanie","shanna","shannah","shannen","shannon","shanon","shanta","shantee","shara","sharai","shari","sharia","sharity","sharl","sharla","sharleen","sharlene","sharline","sharon","sharona","sharron","sharyl","shaun","shauna","shawn","shawna","shawnee","shay","shayla","shaylah","shaylyn","shaylynn","shayna","shayne","shea","sheba","sheela","sheelagh","sheelah","sheena","sheeree","sheila","sheila-kathryn","sheilah","shel","shela","shelagh","shelba","shelbi","shelby","shelia","shell","shelley","shelli","shellie","shelly","shena","sher","sheree","sheri","sherie","sherill","sherilyn","sherline","sherri","sherrie","sherry","sherye","sheryl","shina","shir","shirl","shirlee","shirleen","shirlene","shirley","shirline","shoshana","shoshanna","siana","sianna","sib","sibbie","sibby","sibeal","sibel","sibella","sibelle","sibilla","sibley","sibyl","sibylla","sibylle","sidoney","sidonia","sidonnie","sigrid","sile","sileas","silva","silvana","silvia","silvie","simona","simone","simonette","simonne","sindee","siobhan","sioux","siouxie","sisely","sisile","sissie","sissy","siusan","sofia","sofie","sondra","sonia","sonja","sonni","sonnie","sonnnie","sonny","sonya","sophey","sophi","sophia","sophie","sophronia","sorcha","sosanna","stace","stacee","stacey","staci","stacia","stacie","stacy","stafani","star","starla","starlene","starlin","starr","stefa","stefania","stefanie","steffane","steffi","steffie","stella","stepha","stephana","stephani","stephanie","stephannie","stephenie","stephi","stephie","stephine","stesha","stevana","stevena","stoddard","storm","stormi","stormie","stormy","sue","suellen","sukey","suki","sula","sunny","sunshine","susan","susana","susanetta","susann","susanna","susannah","susanne","susette","susi","susie","susy","suzann","suzanna","suzanne","suzette","suzi","suzie","suzy","sybil","sybila","sybilla","sybille","sybyl","sydel","sydelle","sydney","sylvia","tabatha","tabbatha","tabbi","tabbie","tabbitha","tabby","tabina","tabitha","taffy","talia","tallia","tallie","tallou","tallulah","tally","talya","talyah","tamar","tamara","tamarah","tamarra","tamera","tami","tamiko","tamma","tammara","tammi","tammie","tammy","tamqrah","tamra","tana","tandi","tandie","tandy","tanhya","tani","tania","tanitansy","tansy","tanya","tara","tarah","tarra","tarrah","taryn","tasha","tasia","tate","tatiana","tatiania","tatum","tawnya","tawsha","ted","tedda","teddi","teddie","teddy","tedi","tedra","teena","teirtza","teodora","tera","teresa","terese","teresina","teresita","teressa","teri","teriann","terra","terri","terrie","terrijo","terry","terrye","tersina","terza","tess","tessa","tessi","tessie","tessy","thalia","thea","theadora","theda","thekla","thelma","theo","theodora","theodosia","theresa","therese","theresina","theresita","theressa","therine","thia","thomasa","thomasin","thomasina","thomasine","tiena","tierney","tiertza","tiff","tiffani","tiffanie","tiffany","tiffi","tiffie","tiffy","tilda","tildi","tildie","tildy","tillie","tilly","tim","timi","timmi","timmie","timmy","timothea","tina","tine","tiphani","tiphanie","tiphany","tish","tisha","tobe","tobey","tobi","toby","tobye","toinette","toma","tomasina","tomasine","tomi","tommi","tommie","tommy","toni","tonia","tonie","tony","tonya","tonye","tootsie","torey","tori","torie","torrie","tory","tova","tove","tracee","tracey","traci","tracie","tracy","trenna","tresa","trescha","tressa","tricia","trina","trish","trisha","trista","trix","trixi","trixie","trixy","truda","trude","trudey","trudi","trudie","trudy","trula","tuesday","twila","twyla","tybi","tybie","tyne","ula","ulla","ulrica","ulrika","ulrikaumeko","ulrike","umeko","una","ursa","ursala","ursola","ursula","ursulina","ursuline","uta","val","valaree","valaria","vale","valeda","valencia","valene","valenka","valentia","valentina","valentine","valera","valeria","valerie","valery","valerye","valida","valina","valli","vallie","vally","valma","valry","van","vanda","vanessa","vania","vanna","vanni","vannie","vanny","vanya","veda","velma","velvet","venita","venus","vera","veradis","vere","verena","verene","veriee","verile","verina","verine","verla","verna","vernice","veronica","veronika","veronike","veronique","vevay","vi","vicki","vickie","vicky","victoria","vida","viki","vikki","vikky","vilhelmina","vilma","vin","vina","vinita","vinni","vinnie","vinny","viola","violante","viole","violet","violetta","violette","virgie","virgina","virginia","virginie","vita","vitia","vitoria","vittoria","viv","viva","vivi","vivia","vivian","viviana","vivianna","vivianne","vivie","vivien","viviene","vivienne","viviyan","vivyan","vivyanne","vonni","vonnie","vonny","vyky","wallie","wallis","walliw","wally","waly","wanda","wandie","wandis","waneta","wanids","wenda","wendeline","wendi","wendie","wendy","wendye","wenona","wenonah","whitney","wileen","wilhelmina","wilhelmine","wilie","willa","willabella","willamina","willetta","willette","willi","willie","willow","willy","willyt","wilma","wilmette","wilona","wilone","wilow","windy","wini","winifred","winna","winnah","winne","winni","winnie","winnifred","winny","winona","winonah","wren","wrennie","wylma","wynn","wynne","wynnie","wynny","xaviera","xena","xenia","xylia","xylina","yalonda","yasmeen","yasmin","yelena","yetta","yettie","yetty","yevette","ynes","ynez","yoko","yolanda","yolande","yolane","yolanthe","yoshi","yoshiko","yovonnda","ysabel","yvette","yvonne","zabrina","zahara","zandra","zaneta","zara","zarah","zaria","zarla","zea","zelda","zelma","zena","zenia","zia","zilvia","zita","zitella","zoe","zola","zonda","zondra","zonnya","zora","zorah","zorana","zorina","zorine","zsa zsa","zsazsa","zulema","zuzana","stay","nature","orders","availability","africa","summary","turn","mean","growth","notes","agency","king","monday","european","activity","copy","although","drug","pics","western","income","force","cash","employment","overall","bay","river","commission","ad","package","contents","seen","players","engine","port","album","regional","stop","supplies","started","administration","bar","institute","views","plans","double","dog","build","screen","exchange","types","soon","sponsored","lines","electronic","continue","across","benefits","needed","season","apply","someone","held","ny","anything","printer","condition","effective","believe","organization","effect","asked","eur","mind","sunday","selection","casino","pdf","lost","tour","menu","volume","cross","anyone","mortgage","hope","silver","corporation","wish","inside","solution","mature","role","rather","weeks","addition","came","supply","nothing","certain","usr","executive","running","lower","necessary","union","jewelry","according","dc","clothing","mon","com","particular","fine","names","robert","homepage","hour","gas","skills","six","bush","islands","advice","career","military","rental","decision","leave","british","teens","pre","huge","sat","woman","facilities","zip","bid","kind","sellers","middle","move","cable","opportunities","taking","values","division","coming","tuesday","object","lesbian","appropriate","machine","logo","length","actually","nice","score","statistics","client","ok","returns","capital","follow","sample","investment","sent","shown","saturday","christmas","england","culture","band","flash","ms","lead","george","choice","went","starting","registration","fri","thursday","courses","consumer","hi","airport","foreign","artist","outside","furniture","levels","channel","letter","mode","phones","ideas","wednesday","structure","fund","summer","allow","degree","contract","button","releases","wed","homes","super","male","matter","custom","virginia","almost","took","located","multiple","asian","distribution","editor","inn","industrial","cause","potential","song","cnet","ltd","los","hp","focus","late","fall","featured","idea","rooms","female","responsible","inc","communications","win","associated","thomas","primary","cancer","numbers","reason","tool","browser","spring","foundation","answer","voice","eg","friendly","schedule","documents","communication","purpose","feature","bed","comes","police","everyone","independent","ip","approach","cameras","brown","physical","operating","hill","maps","medicine","deal","hold","ratings","chicago","forms","glass","happy","tue","smith","wanted","developed","thank","safe","unique","survey","prior","telephone","sport","ready","feed","animal","sources","mexico","population","pa","regular","secure","navigation","operations","therefore","ass","simply","evidence","station","christian","round","paypal","favorite","understand","option","master","valley","recently","probably","thu","rentals","sea","built","publications","blood","cut","worldwide","improve","connection","publisher","hall","larger","anti","networks","earth","parents","nokia","impact","transfer","introduction","kitchen","strong","tel","carolina","wedding","properties","hospital","ground","overview","ship","accommodation","owners","disease","tx","excellent","paid","italy","perfect","hair","opportunity","kit","classic","basis","command","cities","william","express","anal","award","distance","tree","peter","assessment","ensure","thus","wall","ie","involved","el","extra","especially","interface","pussy","partners","budget","rated","guides","success","maximum","ma","operation","existing","quite","selected","boy","amazon","patients","restaurants","beautiful","warning","wine","locations","horse","vote","forward","flowers","stars","significant","lists","technologies","owner","retail","animals","useful","directly","manufacturer","ways","est","son","providing","rule","mac","housing","takes","iii","gmt","bring","catalog","searches","max","trying","mother","authority","considered","told","xml","traffic","programme","joined","input","strategy","feet","agent","valid","bin","modern","senior","ireland","sexy","teaching","door","grand","testing","trial","charge","units","instead","canadian","cool","normal","wrote","enterprise","ships","entire","educational","md","leading","metal","positive","fl","fitness","chinese","opinion","mb","asia","football","abstract","uses","output","funds","mr","greater","likely","develop","employees","artists","alternative","processing","responsibility","resolution","java","guest","seems","publication","pass","relations","trust","van","contains","session","multi","photography","republic","fees","components","vacation","century","academic","assistance","completed","skin","graphics","indian","prev","ads","mary","il","expected","ring","grade","dating","pacific","mountain","organizations","pop","filter","mailing","vehicle","longer","consider","int","northern","behind","panel","floor","german","buying","match","proposed","default","require","iraq","boys","outdoor","deep","morning","otherwise","allows","rest","protein","plant","reported","hit","transportation","mm","pool","mini","politics","partner","disclaimer","authors","boards","faculty","parties","fish","membership","mission","eye","string","sense","modified","pack","released","stage","internal","goods","recommended","born","unless","richard","detailed","japanese","race","approved","background","target","except","character","usb","maintenance","ability","maybe","functions","ed","moving","brands","places","php","pretty","trademarks","phentermine","spain","southern","yourself","etc","winter","rape","battery","youth","pressure","submitted","boston","incest","debt","keywords","medium","television","interested","core","break","purposes","throughout","sets","dance","wood","msn","itself","defined","papers","playing","awards","fee","studio","reader","virtual","device","established","answers","rent","las","remote","dark","programming","external","apple","le","regarding","instructions","min","offered","theory","enjoy","remove","aid","surface","minimum","visual","host","variety","teachers","isbn","martin","manual","block","subjects","agents","increased","repair","fair","civil","steel","understanding","songs","fixed","wrong","beginning","hands","associates","finally","az","updates","desktop","classes","paris","ohio","gets","sector","capacity","requires","jersey","un","fat","fully","father","electric","saw","instruments","quotes","officer","driver","businesses","dead","respect","unknown","specified","restaurant","mike","trip","pst","worth","mi","procedures","poor","teacher","xxx","eyes","relationship","workers","farm","fucking","georgia","peace","traditional","campus","tom","showing","creative","coast","benefit","progress","funding","devices","lord","grant","sub","agree","fiction","hear","sometimes","watches","careers","beyond","goes","families","led","museum","themselves","fan","transport","interesting","blogs","wife","evaluation","accepted","former","implementation","ten","hits","zone","complex","th","cat","galleries","references","die","presented","jack","flat","flow","agencies","literature","respective","parent","spanish","michigan","columbia","setting","dr","scale","stand","economy","highest","helpful","monthly","critical","frame","musical","definition","secretary","angeles","networking","path","australian","employee","chief","gives","kb","bottom","magazines","packages","detail","francisco","laws","changed","pet","heard","begin","individuals","colorado","royal","clean","switch","russian","largest","african","guy","titles","relevant","guidelines","justice","connect","bible","dev","cup","basket","applied","weekly","vol","installation","described","demand","pp","suite","vegas","na","square","chris","attention","advance","skip","diet","army","auction","gear","lee","os","difference","allowed","correct","charles","nation","selling","lots","piece","sheet","firm","seven","older","illinois","regulations","elements","species","jump","cells","module","resort","facility","random","pricing","dvds","certificate","minister","motion","looks","fashion","directions","visitors","documentation","monitor","trading","forest","calls","whose","coverage","couple","giving","chance","vision","ball","ending","clients","actions","listen","discuss","accept","automotive","naked","goal","successful","sold","wind","communities","clinical","situation","sciences","markets","lowest","highly","publishing","appear","emergency","developing","lives","currency","leather","determine","milf","temperature","palm","announcements","patient","actual","historical","stone","bob","commerce","ringtones","perhaps","persons","difficult","scientific","satellite","fit","tests","village","accounts","amateur","ex","met","pain","xbox","particularly","factors","coffee","www","settings","cum","buyer","cultural","steve","easily","oral","ford","poster","edge","functional","root","au","fi","closed","holidays","ice","pink","zealand","balance","monitoring","graduate","replies","shot","nc","architecture","initial","label","thinking","scott","llc","sec","recommend","canon","hardcore","league","waste","minute","bus","provider","optional","dictionary","cold","accounting","manufacturing","sections","chair","fishing","effort","phase","fields","bag","fantasy","po","letters","motor","va","professor","context","install","shirt","apparel","generally","continued","foot","mass","crime","count","breast","techniques","ibm","rd","johnson","sc","quickly","dollars","websites","religion","claim","driving","permission","surgery","patch","heat","wild","measures","generation","kansas","miss","chemical","doctor","task","reduce","brought","himself","nor","component","enable","exercise","bug","santa","mid","guarantee","leader","diamond","israel","se","processes","soft","servers","alone","meetings","seconds","jones","arizona","keyword","interests","flight","congress","fuel","username","walk","fuck","produced","italian","paperback","classifieds","wait","supported","pocket","saint","rose","freedom","argument","competition","creating","jim","drugs","joint","premium","providers","fresh","characters","attorney","upgrade","di","factor","growing","thousands","km","stream","apartments","pick","hearing","eastern","auctions","therapy","entries","dates","generated","signed","upper","administrative","serious","prime","samsung","limit","began","louis","steps","errors","shops","bondage","del","efforts","informed","ga","ac","thoughts","creek","ft","worked","quantity","urban","practices","sorted","reporting","essential","myself","tours","platform","load","affiliate","labor","immediately","admin","nursing","defense","machines","designated","tags","heavy","covered","recovery","joe","guys","integrated","configuration","cock","merchant","comprehensive","expert","universal","protect","drop","solid","cds","presentation","languages","became","orange","compliance","vehicles","prevent","theme","rich","im","campaign","marine","improvement","vs","guitar","finding","pennsylvania","examples","ipod","saying","spirit","ar","claims","porno","challenge","motorola","acceptance","strategies","mo","seem","affairs","touch","intended","towards","sa","goals","hire","election","suggest","branch","charges","serve","affiliates","reasons","magic","mount","smart","talking","gave","ones","latin","multimedia","xp","tits","avoid","certified","manage","corner","rank","computing","oregon","element","birth","virus","abuse","interactive","requests","separate","quarter","procedure","leadership","tables","define","racing","religious","facts","breakfast","kong","column","plants","faith","chain","developer","identify","avenue","missing","died","approximately","domestic","sitemap","recommendations","moved","houston","reach","comparison","mental","viewed","moment","extended","sequence","inch","attack","sorry","centers","opening","damage","lab","reserve","recipes","cvs","gamma","plastic","produce","snow","placed","truth","counter","failure","follows","eu","weekend","dollar","camp","ontario","automatically","des","minnesota","films","bridge","native","fill","williams","movement","printing","baseball","owned","approval","draft","chart","played","contacts","cc","jesus","readers","clubs","lcd","wa","jackson","equal","adventure","matching","offering","shirts","profit","leaders","posters","institutions","assistant","variable","ave","dj","advertisement","expect","parking","headlines","yesterday","compared","determined","wholesale","workshop","russia","gone","codes","kinds","extension","seattle","statements","golden","completely","teams","fort","cm","wi","lighting","senate","forces","funny","brother","gene","turned","portable","tried","electrical","applicable","disc","returned","pattern","ct","hentai","boat","named","theatre","laser","earlier","manufacturers","sponsor","classical","icon","warranty","dedicated","indiana","direction","harry","basketball","objects","ends","delete","evening","assembly","nuclear","taxes","mouse","signal","criminal","issued","brain","sexual","wisconsin","powerful","dream","obtained","false","da","cast","flower","felt","personnel","passed","supplied","identified","falls","pic","soul","aids","opinions","promote","stated","stats","hawaii","professionals","appears","carry","flag","decided","nj","covers","hr","em","advantage","hello","designs","maintain","tourism","priority","newsletters","adults","clips","savings","iv","graphic","atom","payments","rw","estimated","binding","brief","ended","winning","eight","anonymous","iron","straight","script","served","wants","miscellaneous","prepared","void","dining","alert","integration","atlanta","dakota","tag","interview","mix","framework","disk","installed","queen","vhs","credits","clearly","fix","handle","sweet","desk","criteria","pubmed","dave","massachusetts","diego","hong","vice","associate","ne","truck","behavior","enlarge","ray","frequently","revenue","measure","changing","votes","du","duty","looked","discussions","bear","gain","festival","laboratory","ocean","flights","experts","signs","lack","depth","iowa","whatever","logged","laptop","vintage","train","exactly","dry","explore","maryland","spa","concept","nearly","eligible","checkout","reality","forgot","handling","origin","knew","gaming","feeds","billion","destination","scotland","faster","intelligence","dallas","bought","con","ups","nations","route","followed","specifications","broken","tripadvisor","frank","alaska","zoom","blow","battle","residential","anime","speak","decisions","industries","protocol","query","clip","partnership","editorial","nt","expression","es","equity","provisions","speech","wire","principles","suggestions","rural","shared","sounds","replacement","tape","strategic","judge","spam","economics","acid","bytes","cent","forced","compatible","fight","apartment","height","null","zero","speaker","filed","gb","netherlands","obtain","bc","consulting","recreation","offices","designer","remain","managed","pr","failed","marriage","roll","korea","banks","fr","participants","secret","bath","aa","kelly","leads","negative","austin","favorites","toronto","theater","springs","missouri","andrew","var","perform","healthy","translation","estimates","font","assets","injury","mt","joseph","ministry","drivers","lawyer","figures","married","protected","proposal","sharing","philadelphia","portal","waiting","birthday","beta","fail","gratis","banking","officials","brian","toward","won","slightly","assist","conduct","contained","lingerie","shemale","legislation","calling","parameters","jazz","serving","bags","profiles","miami","comics","matters","houses","doc","postal","relationships","tennessee","wear","controls","breaking","combined","ultimate","wales","representative","frequency","introduced","minor","finish","departments","residents","noted","displayed","mom","reduced","physics","rare","spent","performed","extreme","samples","davis","daniel","bars","reviewed","row","oz","forecast","removed","helps","singles","administrator","cycle","amounts","contain","accuracy","dual","rise","usd","sleep","mg","bird","pharmacy","brazil","creation","static","scene","hunter","addresses","lady","crystal","famous","writer","chairman","violence","fans","oklahoma","speakers","drink","academy","dynamic","gender","eat","permanent","agriculture","dell","cleaning","constitutes","portfolio","practical","delivered","collectibles","infrastructure","exclusive","seat","concerns","colour","vendor","originally","intel","utilities","philosophy","regulation","officers","reduction","aim","bids","referred","supports","nutrition","recording","regions","junior","toll","les","cape","ann","rings","meaning","tip","secondary","wonderful","mine","ladies","henry","ticket","announced","guess","agreed","prevention","whom","ski","soccer","math","import","posting","presence","instant","mentioned","automatic","healthcare","viewing","maintained","ch","increasing","majority","connected","christ","dan","dogs","sd","directors","aspects","austria","ahead","moon","participation","scheme","utility","preview","fly","manner","matrix","containing","combination","devel","amendment","despite","strength","guaranteed","turkey","libraries","proper","distributed","degrees","singapore","enterprises","delta","fear","seeking","inches","phoenix","rs","convention","shares","principal","daughter","standing","voyeur","comfort","colors","wars","cisco","ordering","kept","alpha","appeal","cruise","bonus","certification","previously","hey","bookmark","buildings","specials","beat","disney","household","batteries","adobe","smoking","bbc","becomes","drives","arms","alabama","tea","improved","trees","avg","achieve","positions","dress","subscription","dealer","contemporary","sky","utah","nearby","rom","carried","happen","exposure","panasonic","hide","permalink","signature","gambling","refer","miller","provision","outdoors","clothes","caused","luxury","babes","frames","viagra","certainly","indeed","newspaper","toy","circuit","layer","printed","slow","removal","easier","src","liability","trademark","hip","printers","faqs","nine","adding","kentucky","mostly","eric","spot","taylor","trackback","prints","spend","factory","interior","revised","grow","americans","optical","promotion","relative","amazing","clock","dot","hiv","identity","suites","conversion","feeling","hidden","reasonable","victoria","serial","relief","revision","broadband","influence","ratio","pda","importance","rain","onto","dsl","planet","webmaster","copies","recipe","zum","permit","seeing","proof","dna","diff","tennis","bass","prescription","bedroom","empty","instance","hole","pets","ride","licensed","orlando","specifically","tim","bureau","maine","sql","represent","conservation","pair","ideal","specs","recorded","don","pieces","finished","parks","dinner","lawyers","sydney","stress","cream","ss","runs","trends","yeah","discover","sexo","ap","patterns","boxes","louisiana","hills","javascript","fourth","nm","advisor","mn","marketplace","nd","evil","aware","wilson","shape","evolution","irish","certificates","objectives","stations","suggested","gps","op","remains","acc","greatest","firms","concerned","euro","operator","structures","generic","encyclopedia","usage","cap","ink","charts","continuing","mixed","census","interracial","peak","tn","competitive","exist","wheel","transit","dick","suppliers","salt","compact","poetry","lights","tracking","angel","bell","keeping","preparation","attempt","receiving","matches","accordance","width","noise","engines","forget","array","discussed","accurate","stephen","elizabeth","climate","reservations","pin","playstation","alcohol","greek","instruction","managing","annotation","sister","raw","differences","walking","explain","smaller","newest","establish","gnu","happened","expressed","jeff","extent","sharp","lesbians","ben","lane","paragraph","kill","mathematics","aol","compensation","ce","export","managers","aircraft","modules","sweden","conflict","conducted","versions","employer","occur","percentage","knows","mississippi","describe","concern","backup","requested","citizens","connecticut","heritage","personals","immediate","holding","trouble","spread","coach","kevin","agricultural","expand","supporting","audience","assigned","jordan","collections","ages","participate","plug","specialist","cook","affect","virgin","experienced","investigation","raised","hat","institution","directed","dealers","searching","sporting","helping","perl","affected","lib","bike","totally","plate","expenses","indicate","blonde","ab","proceedings","favourite","transmission","anderson","utc","characteristics","der","lose","organic","seek","experiences","albums","cheats","extremely","verzeichnis","contracts","guests","hosted","diseases","concerning","developers","equivalent","chemistry","tony","neighborhood","nevada","kits","thailand","variables","agenda","anyway","continues","tracks","advisory","cam","curriculum","logic","template","prince","circle","soil","grants","anywhere","psychology","responses","atlantic","wet","circumstances","edward","investor","identification","ram","leaving","wildlife","appliances","matt","elementary","cooking","speaking","sponsors","fox","unlimited","respond","sizes","plain","exit","entered","iran","arm","keys","launch","wave","checking","costa","belgium","printable","holy","acts","guidance","mesh","trail","enforcement","symbol","crafts","highway","buddy","hardcover","observed","dean","setup","poll","booking","glossary","fiscal","celebrity","styles","denver","unix","filled","bond","channels","ericsson","appendix","notify","blues","chocolate","pub","portion","scope","hampshire","supplier","cables","cotton","bluetooth","controlled","requirement","authorities","biology","dental","killed","border","ancient","debate","representatives","starts","pregnancy","causes","arkansas","biography","leisure","attractions","learned","transactions","notebook","explorer","historic","attached","opened","tm","husband","disabled","authorized","crazy","upcoming","britain","concert","retirement","scores","financing","efficiency","sp","comedy","adopted","efficient","weblog","linear","commitment","specialty","bears","jean","hop","carrier","edited","constant","visa","mouth","jewish","meter","linked","portland","interviews","concepts","nh","gun","reflect","pure","deliver","wonder","hell","lessons","fruit","begins","qualified","reform","lens","alerts","treated","discovery","draw","mysql","classified","relating","assume","confidence","alliance","fm","confirm","warm","neither","lewis","howard","offline","leaves","engineer","lifestyle","consistent","replace","clearance","connections","inventory","converter","suck","organisation","babe","checks","reached","becoming","blowjob","safari","objective","indicated","sugar","crew","legs","sam","stick","securities","allen","pdt","relation","enabled","genre","slide","montana","volunteer","tested","rear","democratic","enhance","switzerland","exact","bound","parameter","adapter","processor","node","formal","dimensions","contribute","lock","hockey","storm","micro","colleges","laptops","mile","showed","challenges","editors","mens","threads","bowl","supreme","brothers","recognition","presents","ref","tank","submission","dolls","estimate","encourage","navy","kid","regulatory","inspection","consumers","cancel","limits","territory","transaction","manchester","weapons","paint","delay","pilot","outlet","contributions","continuous","db","czech","resulting","cambridge","initiative","novel","pan","execution","disability","increases","ultra","winner","idaho","contractor","ph","episode","examination","potter","dish","plays","bulletin","ia","pt","indicates","modify","oxford","adam","truly","epinions","painting","committed","extensive","affordable","universe","candidate","databases","patent","slot","psp","outstanding","ha","eating","perspective","planned","watching","lodge","messenger","mirror","tournament","consideration","ds","discounts","sterling","sessions","kernel","boobs","stocks","buyers","journals","gray","catalogue","ea","jennifer","antonio","charged","broad","taiwan","und","chosen","demo","greece","lg","swiss","sarah","clark","labour","hate","terminal","publishers","nights","behalf","caribbean","liquid","rice","nebraska","loop","salary","reservation","foods","gourmet","guard","properly","orleans","saving","nfl","remaining","empire","resume","twenty","newly","raise","prepare","avatar","gary","depending","illegal","expansion","vary","hundreds","rome","arab","lincoln","helped","premier","tomorrow","purchased","milk","decide","consent","drama","visiting","performing","downtown","keyboard","contest","collected","nw","bands","boot","suitable","ff","absolutely","millions","lunch","dildo","audit","push","chamber","guinea","findings","muscle","featuring","iso","implement","clicking","scheduled","polls","typical","tower","yours","sum","misc","calculator","significantly","chicken","temporary","attend","shower","alan","sending","jason","tonight","dear","sufficient","holdem","shell","province","catholic","oak","vat","awareness","vancouver","governor","beer","seemed","contribution","measurement","swimming","spyware","formula","constitution","packaging","solar","jose","catch","jane","pakistan","ps","reliable","consultation","northwest","sir","doubt","earn","finder","unable","periods","classroom","tasks","democracy","attacks","kim","wallpaper","merchandise","const","resistance","doors","symptoms","resorts","biggest","memorial","visitor","twin","forth","insert","baltimore","gateway","ky","dont","alumni","drawing","candidates","charlotte","ordered","biological","fighting","transition","happens","preferences","spy","romance","instrument","bruce","split","themes","powers","heaven","br","bits","pregnant","twice","classification","focused","egypt","physician","hollywood","bargain","wikipedia","cellular","norway","vermont","asking","blocks","normally","lo","spiritual","hunting","diabetes","suit","ml","shift","chip","res","sit","bodies","photographs","cutting","wow","simon","writers","marks","flexible","loved","favourites","mapping","numerous","relatively","birds","satisfaction","represents","char","indexed","pittsburgh","superior","preferred","saved","paying","cartoon","shots","intellectual","moore","granted","choices","carbon","spending","comfortable","magnetic","interaction","listening","effectively","registry","crisis","outlook","massive","denmark","employed","bright","treat","header","cs","poverty","formed","piano","echo","que","grid","sheets","patrick","experimental","puerto","revolution","consolidation","displays","plasma","allowing","earnings","voip","mystery","landscape","dependent","mechanical","journey","delaware","bidding","consultants","risks","banner","applicant","charter","fig","barbara","cooperation","counties","acquisition","ports","implemented","sf","directories","recognized","dreams","blogger","notification","kg","licensing","stands","teach","occurred","textbooks","rapid","pull","hairy","diversity","cleveland","ut","reverse","deposit","seminar","investments","latina","nasa","wheels","sexcam","specify","accessibility","dutch","sensitive","templates","formats","tab","depends","boots","holds","router","concrete","si","editing","poland","folder","womens","css","completion","upload","pulse","universities","technique","contractors","milfhunter","voting","courts","notices","subscriptions","calculate","mc","detroit","alexander","broadcast","converted","metro","toshiba","anniversary","improvements","strip","specification","pearl","accident","nick","accessible","accessory","resident","plot","qty","possibly","airline","typically","representation","regard","pump","exists","arrangements","smooth","conferences","uniprotkb","beastiality","strike","consumption","birmingham","flashing","lp","narrow","afternoon","threat","surveys","sitting","putting","consultant","controller","ownership","committees","penis","legislative","researchers","vietnam","trailer","anne","castle","gardens","missed","malaysia","unsubscribe","antique","labels","willing","bio","molecular","upskirt","acting","heads","stored","exam","logos","residence","attorneys","milfs","antiques","density","hundred","ryan","operators","strange","sustainable","philippines","statistical","beds","breasts","mention","innovation","pcs","employers","grey","parallel","honda","amended","operate","bills","bold","bathroom","stable","opera","definitions","von","doctors","lesson","cinema","asset","ag","scan","elections","drinking","blowjobs","reaction","blank","enhanced","entitled","severe","generate","stainless","newspapers","hospitals","vi","deluxe","humor","aged","monitors","exception","lived","duration","bulk","successfully","indonesia","pursuant","sci","fabric","edt","visits","primarily","tight","domains","capabilities","pmid","contrast","recommendation","flying","recruitment","sin","berlin","cute","organized","ba","para","siemens","adoption","improving","cr","expensive","meant","capture","pounds","buffalo","organisations","plane","pg","explained","seed","programmes","desire","expertise","mechanism","camping","ee","jewellery","meets","welfare","peer","caught","eventually","marked","driven","measured","medline","bottle","agreements","considering","innovative","marshall","massage","rubber","conclusion","closing","tampa","thousand","meat","legend","grace","susan","ing","ks","adams","python","monster","alex","bang","villa","bone","columns","disorders","bugs","collaboration","hamilton","detection","ftp","cookies","inner","formation","tutorial","med","engineers","entity","cruises","gate","holder","proposals","moderator","sw","tutorials","settlement","portugal","lawrence","roman","duties","valuable","erotic","tone","collectables","ethics","forever","dragon","busy","captain","fantastic","imagine","brings","heating","leg","neck","hd","wing","governments","purchasing","scripts","abc","stereo","appointed","taste","dealing","commit","tiny","operational","rail","airlines","liberal","livecam","jay","trips","gap","sides","tube","turns","corresponding","descriptions","cache","belt","jacket","determination","animation","oracle","er","matthew","lease","productions","aviation","hobbies","proud","excess","disaster","console","commands","jr","telecommunications","instructor","giant","achieved","injuries","shipped","bestiality","seats","approaches","biz","alarm","voltage","anthony","nintendo","usual","loading","stamps","appeared","franklin","angle","rob","vinyl","highlights","mining","designers","melbourne","ongoing","worst","imaging","betting","scientists","liberty","wyoming","blackjack","argentina","era","convert","possibility","analyst","commissioner","dangerous","garage","exciting","reliability","thongs","gcc","unfortunately","respectively","volunteers","attachment","ringtone","finland","morgan","derived","pleasure","honor","asp","oriented","eagle","desktops","pants","columbus","nurse","prayer","appointment","workshops","hurricane","quiet","luck","postage","producer","represented","mortgages","dial","responsibilities","cheese","comic","carefully","jet","productivity","investors","crown","par","underground","diagnosis","maker","crack","principle","picks","vacations","gang","semester","calculated","cumshot","fetish","applies","casinos","appearance","smoke","apache","filters","incorporated","nv","craft","cake","notebooks","apart","fellow","blind","lounge","mad","algorithm","semi","coins","andy","gross","strongly","cafe","valentine","hilton","ken","proteins","horror","su","exp","familiar","capable","douglas","debian","till","involving","pen","investing","christopher","admission","epson","shoe","elected","carrying","victory","sand","madison","terrorism","joy","editions","cpu","mainly","ethnic","ran","parliament","actor","finds","seal","situations","fifth","allocated","citizen","vertical","corrections","structural","municipal","describes","prize","sr","occurs","jon","absolute","disabilities","consists","anytime","substance","prohibited","addressed","lies","pipe","soldiers","nr","guardian","lecture","simulation","layout","initiatives","ill","concentration","classics","lbs","lay","interpretation","horses","lol","dirty","deck","wayne","donate","taught","bankruptcy","mp","worker","optimization","alive","temple","substances","prove","discovered","wings","breaks","genetic","restrictions","participating","waters","promise","thin","exhibition","prefer","ridge","cabinet","modem","harris","mph","bringing","sick","dose","evaluate","tiffany","tropical","collect","bet","composition","toyota","streets","nationwide","vector","definitely","shaved","turning","buffer","purple","existence","commentary","larry","limousines","developments","def","immigration","destinations","lets","mutual","pipeline","necessarily","syntax","li","attribute","prison","skill","chairs","nl","everyday","apparently","surrounding","mountains","moves","popularity","inquiry","ethernet","checked","exhibit","throw","trend","sierra","visible","cats","desert","postposted","ya","oldest","rhode","nba","busty","coordinator","obviously","mercury","steven","handbook","greg","navigate","worse","summit","victims","epa","spaces","fundamental","burning","escape","coupons","somewhat","receiver","substantial","tr","progressive","cialis","bb","boats","glance","scottish","championship","arcade","richmond","sacramento","impossible","ron","russell","tells","obvious","fiber","depression","graph","covering","platinum","judgment","bedrooms","talks","filing","foster","modeling","passing","awarded","testimonials","trials","tissue","nz","memorabilia","clinton","masters","bonds","cartridge","alberta","explanation","folk","org","commons","cincinnati","subsection","fraud","electricity","permitted","spectrum","arrival","okay","pottery","emphasis","roger","aspect","workplace","awesome","mexican","confirmed","counts","priced","wallpapers","hist","crash","lift","desired","inter","closer","assumes","heights","shadow","riding","infection","firefox","lisa","expense","grove","eligibility","venture","clinic","korean","healing","princess","mall","entering","packet","spray","studios","involvement","dad","buttons","placement","observations","vbulletin","funded","thompson","winners","extend","roads","subsequent","pat","dublin","rolling","fell","motorcycle","yard","disclosure","establishment","memories","nelson","te","arrived","creates","faces","tourist","cocks","av","mayor","murder","sean","adequate","senator","yield","presentations","grades","cartoons","pour","digest","reg","lodging","tion","dust","hence","wiki","entirely","replaced","radar","rescue","undergraduate","losses","combat","reducing","stopped","occupation","lakes","butt","donations","associations","citysearch","closely","radiation","diary","seriously","kings","shooting","kent","adds","nsw","ear","flags","pci","baker","launched","elsewhere","pollution","conservative","guestbook","shock","effectiveness","walls","abroad","ebony","tie","ward","drawn","arthur","ian","visited","roof","walker","demonstrate","atmosphere","suggests","kiss","beast","ra","operated","experiment","targets","overseas","purchases","dodge","counsel","federation","pizza","invited","yards","assignment","chemicals","gordon","mod","farmers","rc","queries","bmw","rush","ukraine","absence","nearest","cluster","vendors","mpeg","whereas","yoga","serves","woods","surprise","lamp","rico","partial","shoppers","phil","everybody","couples","nashville","ranking","jokes","cst","http","ceo","simpson","twiki","sublime","counseling","palace","acceptable","satisfied","glad","wins","measurements","verify","globe","trusted","copper","milwaukee","rack","medication","warehouse","shareware","ec","rep","dicke","kerry","receipt","supposed","ordinary","nobody","ghost","violation","configure","stability","mit","applying","southwest","boss","pride","institutional","expectations","independence","knowing","reporter","metabolism","keith","champion","cloudy","linda","ross","personally","chile","anna","plenty","solo","sentence","throat","ignore","maria","uniform","excellence","wealth","tall","rm","somewhere","vacuum","dancing","attributes","recognize","brass","writes","plaza","pdas","outcomes","survival","quest","publish","sri","screening","toe","thumbnail","trans","jonathan","whenever","nova","lifetime","api","pioneer","booty","forgotten","acrobat","plates","acres","venue","athletic","thermal","essays","behaviour","vital","telling","fairly","coastal","config","cf","charity","intelligent","edinburgh","vt","excel","modes","obligation","campbell","wake","stupid","harbor","hungary","traveler","urw","segment","realize","regardless","lan","enemy","puzzle","rising","aluminum","wells","wishlist","opens","insight","sms","shit","restricted","republican","secrets","lucky","latter","merchants","thick","trailers","repeat","syndrome","philips","attendance","penalty","drum","glasses","enables","nec","iraqi","builder","vista","jessica","chips","terry","flood","foto","ease","arguments","amsterdam","orgy","arena","adventures","pupils","stewart","announcement","tabs","outcome","xx","appreciate","expanded","casual","grown","polish","lovely","extras","gm","centres","jerry","clause","smile","lands","ri","troops","indoor","bulgaria","armed","broker","charger","regularly","believed","pine","cooling","tend","gulf","rt","rick","trucks","cp","mechanisms","divorce","laura","shopper","tokyo","partly","nikon","customize","tradition","candy","pills","tiger","donald","folks","sensor","exposed","telecom","hunt","angels","deputy","indicators","sealed","thai","emissions","physicians","loaded","fred","complaint","scenes","experiments","balls","afghanistan","dd","boost","spanking","scholarship","governance","mill","founded","supplements","chronic","icons","tranny","moral","den","catering","aud","finger","keeps","pound","locate","camcorder","pl","trained","burn","implementing","roses","labs","ourselves","bread","tobacco","wooden","motors","tough","roberts","incident","gonna","dynamics","lie","crm","rf","conversation","decrease","cumshots","chest","pension","billy","revenues","emerging","worship","bukkake","capability","ak","fe","craig","herself","producing","churches","precision","damages","reserves","contributed","solve","shorts","reproduction","minority","td","diverse","amp","ingredients","sb","ah","johnny","sole","franchise","recorder","complaints","facing","sm","nancy","promotions","tones","passion","rehabilitation","maintaining","sight","laid","clay","defence","patches","weak","refund","usc","towns","environments","trembl","divided","blvd","reception","amd","wise","emails","cyprus","wv","odds","correctly","insider","seminars","consequences","makers","hearts","geography","appearing","integrity","worry","ns","discrimination","eve","carter","legacy","marc","pleased","danger","vitamin","widely","processed","phrase","genuine","raising","implications","functionality","paradise","hybrid","reads","roles","intermediate","emotional","sons","leaf","pad","glory","platforms","ja","bigger","billing","diesel","versus","combine","overnight","geographic","exceed","bs","rod","saudi","fault","cuba","hrs","preliminary","districts","introduce","silk","promotional","kate","chevrolet","babies","bi","karen","compiled","romantic","revealed","specialists","generator","albert","examine","jimmy","graham","suspension","bristol","margaret","compaq","sad","correction","wolf","slowly","authentication","communicate","rugby","supplement","showtimes","cal","portions","infant","promoting","sectors","samuel","fluid","grounds","fits","kick","regards","meal","ta","hurt","machinery","bandwidth","unlike","equation","baskets","probability","pot","dimension","wright","img","barry","proven","schedules","admissions","cached","warren","slip","studied","reviewer","involves","quarterly","rpm","profits","devil","grass","comply","marie","florist","illustrated","cherry","continental","alternate","deutsch","achievement","limitations","kenya","webcam","cuts","funeral","nutten","earrings","enjoyed","automated","chapters","pee","charlie","quebec","nipples","passenger","convenient","dennis","mars","francis","tvs","sized","manga","noticed","socket","silent","literary","egg","mhz","signals","caps","orientation","pill","theft","childhood","swing","symbols","lat","meta","humans","analog","facial","choosing","talent","dated","flexibility","seeker","wisdom","shoot","boundary","mint","packard","offset","payday","philip","elite","gi","spin","holders","believes","swedish","poems","deadline","jurisdiction","robot","displaying","witness","collins","equipped","stages","encouraged","sur","winds","powder","broadway","acquired","assess","wash","cartridges","stones","entrance","gnome","roots","declaration","losing","attempts","gadgets","noble","glasgow","automation","impacts","rev","gospel","advantages","shore","loves","induced","ll","knight","preparing","loose","aims","recipient","linking","extensions","appeals","cl","earned","illness","islamic","athletics","southeast","ieee","ho","alternatives","pending","parker","determining","lebanon","corp","personalized","kennedy","gt","sh","conditioning","teenage","soap","ae","triple","cooper","nyc","vincent","jam","secured","unusual","answered","partnerships","destruction","slots","increasingly","migration","disorder","routine","toolbar","basically","rocks","conventional","titans","applicants","wearing","axis","sought","genes","mounted","habitat","firewall","median","guns","scanner","herein","occupational","animated","horny","judicial","rio","hs","adjustment","hero","integer","treatments","bachelor","attitude","camcorders","engaged","falling","basics","montreal","carpet","rv","struct","lenses","binary","genetics","attended","difficulty","punk","collective","coalition","pi","dropped","enrollment","duke","walter","ai","pace","besides","wage","producers","ot","collector","arc","hosts","interfaces","advertisers","moments","atlas","strings","dawn","representing","observation","feels","torture","carl","deleted","coat","mitchell","mrs","rica","restoration","convenience","returning","ralph","opposition","container","yr","defendant","warner","confirmation","app","embedded","inkjet","supervisor","wizard","corps","actors","liver","peripherals","liable","brochure","morris","bestsellers","petition","eminem","recall","antenna","picked","assumed","departure","minneapolis","belief","killing","bikini","memphis","shoulder","decor","lookup","texts","harvard","brokers","roy","ion","diameter","ottawa","doll","ic","podcast","tit","seasons","peru","interactions","refine","bidder","singer","evans","herald","literacy","fails","aging","nike","intervention","pissing","fed","plugin","attraction","diving","invite","modification","alice","latinas","suppose","customized","reed","involve","moderate","terror","younger","thirty","mice","opposite","understood","rapidly","dealtime","ban","temp","intro","mercedes","zus","assurance","fisting","clerk","happening","vast","mills","outline","amendments","tramadol","holland","receives","jeans","metropolitan","compilation","verification","fonts","ent","odd","wrap","refers","mood","favor","veterans","quiz","mx","sigma","gr","attractive","xhtml","occasion","recordings","jefferson","victim","demands","sleeping","careful","ext","beam","gardening","obligations","arrive","orchestra","sunset","tracked","moreover","minimal","polyphonic","lottery","tops","framed","aside","outsourcing","licence","adjustable","allocation","michelle","essay","discipline","amy","ts","demonstrated","dialogue","identifying","alphabetical","camps","declared","dispatched","aaron","handheld","trace","disposal","shut","florists","packs","ge","installing","switches","romania","voluntary","ncaa","thou","consult","phd","greatly","blogging","mask","cycling","midnight","ng","commonly","pe","photographer","inform","turkish","coal","cry","messaging","pentium","quantum","murray","intent","tt","zoo","largely","pleasant","announce","constructed","additions","requiring","spoke","aka","arrow","engagement","sampling","rough","weird","tee","refinance","lion","inspired","holes","weddings","blade","suddenly","oxygen","cookie","meals","canyon","goto","meters","merely","calendars","arrangement","conclusions","passes","bibliography","pointer","compatibility","stretch","durham","furthermore","permits","cooperative","muslim","xl","neil","sleeve","netscape","cleaner","cricket","beef","feeding","stroke","township","rankings","measuring","cad","hats","robin","robinson","jacksonville","strap","headquarters","sharon","crowd","tcp","transfers","surf","olympic","transformation","remained","attachments","dv","dir","entities","customs","administrators","personality","rainbow","hook","roulette","decline","gloves","israeli","medicare","cord","skiing","cloud","facilitate","subscriber","valve","val","hewlett","explains","proceed","flickr","feelings","knife","jamaica","priorities","shelf","bookstore","timing","liked","parenting","adopt","denied","fotos","incredible","britney","freeware","fucked","donation","outer","crop","deaths","rivers","commonwealth","pharmaceutical","manhattan","tales","katrina","workforce","islam","nodes","tu","fy","thumbs","seeds","cited","lite","ghz","hub","targeted","organizational","skype","realized","twelve","founder","decade","gamecube","rr","dispute","portuguese","tired","titten","adverse","everywhere","excerpt","eng","steam","discharge","ef","drinks","ace","voices","acute","halloween","climbing","stood","sing","tons","perfume","carol","honest","albany","hazardous","restore","stack","methodology","somebody","sue","ep","housewares","reputation","resistant","democrats","recycling","hang","gbp","curve","creator","amber","qualifications","museums","coding","slideshow","tracker","variation","passage","transferred","trunk","hiking","lb","damn","pierre","jelsoft","headset","photograph","oakland","colombia","waves","camel","distributor","lamps","underlying","hood","wrestling","suicide","archived","photoshop","jp","chi","bt","arabia","gathering","projection","juice","chase","mathematical","logical","sauce","fame","extract","specialized","diagnostic","panama","indianapolis","af","payable","corporations","courtesy","criticism","automobile","confidential","rfc","statutory","accommodations","athens","northeast","downloaded","judges","sl","seo","retired","isp","remarks","detected","decades","paintings","walked","arising","nissan","bracelet","ins","eggs","juvenile","injection","yorkshire","populations","protective","afraid","acoustic","railway","cassette","initially","indicator","pointed","hb","jpg","causing","mistake","norton","locked","eliminate","tc","fusion","mineral","sunglasses","ruby","steering","beads","fortune","preference","canvas","threshold","parish","claimed","screens","cemetery","planner","croatia","flows","stadium","venezuela","exploration","mins","fewer","sequences","coupon","nurses","ssl","stem","proxy","gangbang","astronomy","lanka","opt","edwards","drew","contests","flu","translate","announces","mlb","costume","tagged","berkeley","voted","killer","bikes","gates","adjusted","rap","tune","bishop","pulled","corn","gp","shaped","compression","seasonal","establishing","farmer","counters","puts","constitutional","grew","perfectly","tin","slave","instantly","cultures","norfolk","coaching","examined","trek","encoding","litigation","submissions","oem","heroes","painted","lycos","ir","zdnet","broadcasting","horizontal","artwork","cosmetic","resulted","portrait","terrorist","informational","ethical","carriers","ecommerce","mobility","floral","builders","ties","struggle","schemes","suffering","neutral","fisher","rat","spears","prospective","dildos","bedding","ultimately","joining","heading","equally","artificial","bearing","spectacular","coordination","connector","brad","combo","seniors","worlds","guilty","affiliated","activation","naturally","haven","tablet","jury","dos","tail","subscribers","charm","lawn","violent","mitsubishi","underwear","basin","soup","potentially","ranch","constraints","crossing","inclusive","dimensional","cottage","drunk","considerable","crimes","resolved","mozilla","byte","toner","nose","latex","branches","anymore","oclc","delhi","holdings","alien","locator","selecting","processors","pantyhose","plc","broke","nepal","zimbabwe","difficulties","juan","complexity","msg","constantly","browsing","resolve","barcelona","presidential","documentary","cod","territories","melissa","moscow","thesis","thru","jews","nylon","palestinian","discs","rocky","bargains","frequent","trim","nigeria","ceiling","pixels","ensuring","hispanic","cv","cb","legislature","hospitality","gen","anybody","procurement","diamonds","espn","fleet","untitled","bunch","totals","marriott","singing","theoretical","afford","exercises","starring","referral","nhl","surveillance","optimal","quit","distinct","protocols","lung","highlight","substitute","inclusion","hopefully","brilliant","turner","sucking","cents","reuters","ti","fc","gel","todd","spoken","omega","evaluated","stayed","civic","assignments","fw","manuals","doug","sees","termination","watched","saver","thereof","grill","households","gs","redeem","rogers","grain","aaa","authentic","regime","wanna","wishes","bull","montgomery","architectural","louisville","depend","differ","macintosh","movements","ranging","monica","repairs","breath","amenities","virtually","cole","mart","candle","hanging","colored","authorization","tale","verified","lynn","formerly","projector","bp","situated","comparative","std","seeks","herbal","loving","strictly","routing","docs","stanley","psychological","surprised","retailer","vitamins","elegant","gains","renewal","vid","genealogy","opposed","deemed","scoring","expenditure","panties","brooklyn","liverpool","sisters","critics","connectivity","spots","oo","algorithms","hacker","madrid","similarly","margin","coin","bbw","solely","fake","salon","collaborative","norman","fda","excluding","turbo","headed","voters","cure","madonna","commander","arch","ni","murphy","thinks","thats","suggestion","hdtv","soldier","phillips","asin","aimed","justin","bomb","harm","interval","mirrors","spotlight","tricks","reset","brush","investigate","thy","expansys","panels","repeated","assault","connecting","spare","logistics","deer","kodak","tongue","bowling","tri","danish","pal","monkey","proportion","filename","skirt","florence","invest","honey","um","analyses","drawings","significance","scenario","ye","fs","lovers","atomic","approx","symposium","arabic","gauge","essentials","junction","protecting","nn","faced","mat","rachel","solving","transmitted","weekends","screenshots","produces","oven","ted","intensive","chains","kingston","sixth","engage","deviant","noon","switching","quoted","adapters","correspondence","farms","imports","supervision","cheat","bronze","expenditures","sandy","separation","testimony","suspect","celebrities","macro","sender","mandatory","boundaries","crucial","syndication","gym","celebration","kde","adjacent","filtering","tuition","spouse","exotic","viewer","signup","threats","luxembourg","puzzles","reaching","vb","damaged","cams","receptor","piss","laugh","joel","surgical","destroy","citation","pitch","autos","yo","premises","perry","proved","offensive","imperial","dozen","benjamin","deployment","teeth","cloth","studying","colleagues","stamp","lotus","salmon","olympus","separated","proc","cargo","tan","directive","fx","salem","mate","dl","starter","upgrades","likes","butter","pepper","weapon","luggage","burden","chef","tapes","zones","races","isle","stylish","slim","maple","luke","grocery","offshore","governing","retailers","depot","kenneth","comp","alt","pie","blend","harrison","ls","julie","occasionally","cbs","attending","emission","pete","spec","finest","realty","janet","bow","penn","recruiting","apparent","instructional","phpbb","autumn","traveling","probe","midi","permissions","biotechnology","toilet","ranked","jackets","routes","packed","excited","outreach","helen","mounting","recover","tied","lopez","balanced","prescribed","catherine","timely","talked","upskirts","debug","delayed","chuck","reproduced","hon","dale","explicit","calculation","villas","ebook","consolidated","boob","exclude","peeing","occasions","brooks","equations","newton","oils","sept","exceptional","anxiety","bingo","whilst","spatial","respondents","unto","lt","ceramic","prompt","precious","minds","annually","considerations","scanners","atm","xanax","eq","pays","cox","fingers","sunny","ebooks","delivers","je","queensland","necklace","musicians","leeds","composite","unavailable","cedar","arranged","lang","theaters","advocacy","raleigh","stud","fold","essentially","designing","threaded","uv","qualify","fingering","blair","hopes","assessments","cms","mason","diagram","burns","pumps","slut","ejaculation","footwear","sg","vic","beijing","peoples","victor","mario","pos","attach","licenses","utils","removing","advised","brunswick","spider","phys","ranges","pairs","sensitivity","trails","preservation","hudson","isolated","calgary","interim","assisted","divine","streaming","approve","chose","compound","intensity","technological","syndicate","abortion","dialog","venues","blast","wellness","calcium","newport","antivirus","addressing","pole","discounted","indians","shield","harvest","membrane","prague","previews","bangladesh","constitute","locally","concluded","pickup","desperate","mothers","nascar","iceland","demonstration","governmental","manufactured","candles","graduation","mega","bend","sailing","variations","moms","sacred","addiction","morocco","chrome","tommy","springfield","refused","brake","exterior","greeting","ecology","oliver","congo","glen","botswana","nav","delays","synthesis","olive","undefined","unemployment","cyber","verizon","scored","enhancement","newcastle","clone","dicks","velocity","lambda","relay","composed","tears","performances","oasis","baseline","cab","angry","fa","societies","silicon","brazilian","identical","petroleum","compete","ist","norwegian","lover","belong","honolulu","beatles","lips","escort","retention","exchanges","pond","rolls","thomson","barnes","soundtrack","wondering","malta","daddy","lc","ferry","rabbit","profession","seating","dam","cnn","separately","physiology","lil","collecting","das","exports","omaha","tire","participant","scholarships","recreational","dominican","chad","electron","loads","friendship","heather","passport","motel","unions","treasury","warrant","sys","solaris","frozen","occupied","josh","royalty","scales","rally","observer","sunshine","strain","drag","ceremony","somehow","arrested","expanding","provincial","investigations","icq","ripe","yamaha","rely","medications","hebrew","gained","rochester","dying","laundry","stuck","solomon","placing","stops","homework","adjust","assessed","advertiser","enabling","encryption","filling","downloadable","sophisticated","imposed","silence","scsi","focuses","soviet","possession","cu","laboratories","treaty","vocal","trainer","organ","stronger","volumes","advances","vegetables","lemon","toxic","dns","thumbnails","darkness","pty","ws","nuts","nail","bizrate","vienna","implied","span","stanford","sox","stockings","joke","respondent","packing","statute","rejected","satisfy","destroyed","shelter","chapel","gamespot","manufacture","layers","wordpress","guided","vulnerability","accountability","celebrate","accredited","appliance","compressed","bahamas","powell","mixture","zoophilia","bench","univ","tub","rider","scheduling","radius","perspectives","mortality","logging","hampton","christians","borders","therapeutic","pads","butts","inns","bobby","impressive","sheep","accordingly","architect","railroad","lectures","challenging","wines","nursery","harder","cups","ash","microwave","cheapest","accidents","travesti","relocation","stuart","contributors","salvador","ali","salad","np","monroe","tender","violations","foam","temperatures","paste","clouds","competitions","discretion","tft","tanzania","preserve","jvc","poem","vibrator","unsigned","staying","cosmetics","easter","theories","repository","praise","jeremy","venice","jo","concentrations","vibrators","estonia","christianity","veteran","streams","landing","signing","executed","katie","negotiations","realistic","dt","cgi","showcase","integral","asks","relax","namibia","generating","christina","congressional","synopsis","hardly","prairie","reunion","composer","bean","sword","absent","photographic","sells","ecuador","hoping","accessed","spirits","modifications","coral","pixel","float","colin","bias","imported","paths","bubble","por","acquire","contrary","millennium","tribune","vessel","acids","focusing","viruses","cheaper","admitted","dairy","admit","mem","fancy","equality","samoa","gc","achieving","tap","stickers","fisheries","exceptions","reactions","leasing","lauren","beliefs","ci","macromedia","companion","squad","analyze","ashley","scroll","relate","divisions","swim","wages","additionally","suffer","forests","fellowship","nano","invalid","concerts","martial","males","victorian","retain","colours","execute","tunnel","genres","cambodia","patents","copyrights","yn","chaos","lithuania","mastercard","wheat","chronicles","obtaining","beaver","updating","distribute","readings","decorative","kijiji","confused","compiler","enlargement","eagles","bases","vii","accused","bee","campaigns","unity","loud","conjunction","bride","rats","defines","airports","instances","indigenous","begun","cfr","brunette","packets","anchor","socks","validation","parade","corruption","stat","trigger","incentives","cholesterol","gathered","essex","slovenia","notified","differential","beaches","folders","dramatic","surfaces","terrible","routers","cruz","pendant","dresses","baptist","scientist","starsmerchant","hiring","clocks","arthritis","bios","females","wallace","nevertheless","reflects","taxation","fever","pmc","cuisine","surely","practitioners","transcript","myspace","theorem","inflation","thee","nb","ruth","pray","stylus","compounds","pope","drums","contracting","topless","arnold","structured","reasonably","jeep","chicks","bare","hung","cattle","mba","radical","graduates","rover","recommends","controlling","treasure","reload","distributors","flame","levitra","tanks","assuming","monetary","elderly","pit","arlington","mono","particles","floating","extraordinary","tile","indicating","bolivia","spell","hottest","stevens","coordinate","kuwait","exclusively","emily","alleged","limitation","widescreen","compile","squirting","webster","struck","rx","illustration","plymouth","warnings","construct","apps","inquiries","bridal","annex","mag","gsm","inspiration","tribal","curious","affecting","freight","rebate","meetup","eclipse","sudan","ddr","downloading","rec","shuttle","aggregate","stunning","cycles","affects","forecasts","detect","sluts","actively","ciao","ampland","knee","prep","pb","complicated","chem","fastest","butler","shopzilla","injured","decorating","payroll","cookbook","expressions","ton","courier","uploaded","shakespeare","hints","collapse","americas","connectors","twinks","unlikely","oe","gif","pros","conflicts","techno","beverage","tribute","wired","elvis","immune","latvia","travelers","forestry","barriers","cant","jd","rarely","gpl","infected","offerings","martha","genesis","barrier","argue","incorrect","trains","metals","bicycle","furnishings","letting","arise","guatemala","celtic","thereby","irc","jamie","particle","perception","minerals","advise","humidity","bottles","boxing","wy","dm","bangkok","renaissance","pathology","sara","bra","ordinance","hughes","photographers","bitch","infections","jeffrey","chess","operates","brisbane","configured","survive","oscar","festivals","menus","joan","possibilities","duck","reveal","canal","amino","phi","contributing","herbs","clinics","mls","cow","manitoba","analytical","missions","watson","lying","costumes","strict","dive","saddam","circulation","drill","offense","threesome","bryan","cet","protest","handjob","assumption","jerusalem","hobby","tries","transexuales","invention","nickname","fiji","technician","inline","executives","enquiries","washing","audi","staffing","cognitive","exploring","trick","enquiry","closure","raid","ppc","timber","volt","intense","div","playlist","registrar","showers","supporters","ruling","steady","dirt","statutes","withdrawal","myers","drops","predicted","wider","saskatchewan","jc","cancellation","plugins","enrolled","sensors","screw","ministers","publicly","hourly","blame","geneva","freebsd","veterinary","acer","prostores","reseller","dist","handed","suffered","intake","informal","relevance","incentive","butterfly","tucson","mechanics","heavily","swingers","fifty","headers","mistakes","numerical","ons","geek","uncle","defining","xnxx","counting","reflection","sink","accompanied","assure","invitation","devoted","princeton","jacob","sodium","randy","spirituality","hormone","meanwhile","proprietary","timothy","childrens","brick","grip","naval","thumbzilla","medieval","porcelain","avi","bridges","pichunter","captured","watt","thehun","decent","casting","dayton","translated","shortly","cameron","columnists","pins","carlos","reno","donna","andreas","warrior","diploma","cabin","innocent","bdsm","scanning","ide","consensus","polo","valium","copying","rpg","delivering","cordless","patricia","horn","eddie","uganda","fired","journalism","pd","prot","trivia","adidas","perth","frog","grammar","intention","syria","disagree","klein","harvey","tires","logs","undertaken","tgp","hazard","retro","leo","livesex","statewide","semiconductor","gregory","episodes","boolean","circular","anger","diy","mainland","illustrations","suits","chances","interact","snap","happiness","arg","substantially","bizarre","glenn","ur","auckland","olympics","fruits","identifier","geo","worldsex","ribbon","calculations","doe","jpeg","conducting","startup","suzuki","trinidad","ati","kissing","wal","handy","swap","exempt","crops","reduces","accomplished","calculators","geometry","impression","abs","slovakia","flip","guild","correlation","gorgeous","capitol","sim","dishes","rna","barbados","chrysler","nervous","refuse","extends","fragrance","mcdonald","replica","plumbing","brussels","tribe","neighbors","trades","superb","buzz","transparent","nuke","rid","trinity","charleston","handled","legends","boom","calm","champions","floors","selections","projectors","inappropriate","exhaust","comparing","shanghai","speaks","burton","vocational","davidson","copied","scotia","farming","gibson","pharmacies","fork","troy","ln","roller","introducing","batch","organize","appreciated","alter","nicole","latino","ghana","edges","uc","mixing","handles","skilled","fitted","albuquerque","harmony","distinguished","asthma","projected","assumptions","shareholders","twins","developmental","rip","zope","regulated","triangle","amend","anticipated","oriental","reward","windsor","zambia","completing","gmbh","buf","ld","hydrogen","webshots","sprint","comparable","chick","advocate","sims","confusion","copyrighted","tray","inputs","warranties","genome","escorts","documented","thong","medal","paperbacks","coaches","vessels","harbour","walks","sucks","sol","keyboards","sage","knives","eco","vulnerable","arrange","artistic","bat","honors","booth","indie","reflected","unified","bones","breed","detector","ignored","polar","fallen","precise","sussex","respiratory","notifications","msgid","transexual","mainstream","invoice","evaluating","lip","subcommittee","sap","gather","suse","maternity","backed","alfred","colonial","mf","carey","motels","forming","embassy","cave","journalists","danny","rebecca","slight","proceeds","indirect","amongst","wool","foundations","msgstr","arrest","volleyball","mw","adipex","horizon","nu","deeply","toolbox","ict","marina","liabilities","prizes","bosnia","browsers","decreased","patio","dp","tolerance","surfing","creativity","lloyd","describing","optics","pursue","lightning","overcome","eyed","ou","quotations","grab","inspector","attract","brighton","beans","bookmarks","ellis","disable","snake","succeed","leonard","lending","oops","reminder","nipple","xi","searched","behavioral","riverside","bathrooms","plains","sku","ht","raymond","insights","abilities","initiated","sullivan","za","midwest","karaoke","trap","lonely","fool","ve","nonprofit","lancaster","suspended","hereby","observe","julia","containers","attitudes","karl","berry","collar","simultaneously","racial","integrate","bermuda","amanda","sociology","mobiles","screenshot","exhibitions","kelkoo","confident","retrieved","exhibits","officially","consortium","dies","terrace","bacteria","pts","replied","seafood","novels","rh","rrp","recipients","playboy","ought","delicious","traditions","fg","jail","safely","finite","kidney","periodically","fixes","sends","durable","mazda","allied","throws","moisture","hungarian","roster","referring","symantec","spencer","wichita","nasdaq","uruguay","ooo","hz","transform","timer","tablets","tuning","gotten","educators","tyler","futures","vegetable","verse","highs","humanities","independently","wanting","custody","scratch","launches","ipaq","alignment","masturbating","henderson","bk","britannica","comm","ellen","competitors","nhs","rocket","aye","bullet","towers","racks","lace","nasty","visibility","latitude","consciousness","ste","tumor","ugly","deposits","beverly","mistress","encounter","trustees","watts","duncan","reprints","hart","bernard","resolutions","ment","accessing","forty","tubes","attempted","col","midlands","priest","floyd","ronald","analysts","queue","dx","sk","trance","locale","nicholas","biol","yu","bundle","hammer","invasion","witnesses","runner","rows","administered","notion","sq","skins","mailed","oc","fujitsu","spelling","arctic","exams","rewards","beneath","strengthen","defend","aj","frederick","medicaid","treo","infrared","seventh","gods","une","welsh","belly","aggressive","tex","advertisements","quarters","stolen","cia","sublimedirectory","soonest","haiti","disturbed","determines","sculpture","poly","ears","dod","wp","fist","naturals","neo","motivation","lenders","pharmacology","fitting","fixtures","bloggers","mere","agrees","passengers","quantities","petersburg","consistently","powerpoint","cons","surplus","elder","sonic","obituaries","cheers","dig","taxi","punishment","appreciation","subsequently","om","belarus","nat","zoning","gravity","providence","thumb","restriction","incorporate","backgrounds","treasurer","guitars","essence","flooring","lightweight","ethiopia","tp","mighty","athletes","humanity","transcription","jm","holmes","complications","scholars","dpi","scripting","gis","remembered","galaxy","chester","snapshot","caring","loc","worn","synthetic","shaw","vp","segments","testament","expo","dominant","twist","specifics","itunes","stomach","partially","buried","cn","newbie","minimize","darwin","ranks","wilderness","debut","generations","tournaments","bradley","deny","anatomy","bali","judy","sponsorship","headphones","fraction","trio","proceeding","cube","defects","volkswagen","uncertainty","breakdown","milton","marker","reconstruction","subsidiary","strengths","clarity","rugs","sandra","adelaide","encouraging","furnished","monaco","settled","folding","emirates","terrorists","airfare","comparisons","beneficial","distributions","vaccine","belize","crap","fate","viewpicture","promised","volvo","penny","robust","bookings","threatened","minolta","republicans","discusses","gui","porter","gras","jungle","ver","rn","responded","rim","abstracts","zen","ivory","alpine","dis","prediction","pharmaceuticals","andale","fabulous","remix","alias","thesaurus","individually","battlefield","literally","newer","kay","ecological","spice","oval","implies","cg","soma","ser","cooler","appraisal","consisting","maritime","periodic","submitting","overhead","ascii","prospect","shipment","breeding","citations","geographical","donor","mozambique","tension","href","benz","trash","shapes","wifi","tier","fwd","earl","manor","envelope","diane","homeland","disclaimers","championships","excluded","andrea","breeds","rapids","disco","sheffield","bailey","aus","endif","finishing","emotions","wellington","incoming","prospects","lexmark","cleaners","bulgarian","hwy","eternal","cashiers","guam","cite","aboriginal","remarkable","rotation","nam","preventing","productive","boulevard","eugene","ix","gdp","pig","metric","compliant","minus","penalties","bennett","imagination","hotmail","refurbished","joshua","armenia","varied","grande","closest","activated","actress","mess","conferencing","assign","armstrong","politicians","trackbacks","lit","accommodate","tigers","aurora","una","slides","milan","premiere","lender","villages","shade","chorus","christine","rhythm","digit","argued","dietary","symphony","clarke","sudden","accepting","precipitation","marilyn","lions","findlaw","ada","pools","tb","lyric","claire","isolation","speeds","sustained","matched","approximate","rope","carroll","rational","programmer","fighters","chambers","dump","greetings","inherited","warming","incomplete","vocals","chronicle","fountain","chubby","grave","legitimate","biographies","burner","yrs","foo","investigator","gba","plaintiff","finnish","gentle","bm","prisoners","deeper","muslims","hose","mediterranean","nightlife","footage","howto","worthy","reveals","architects","saints","entrepreneur","carries","sig","freelance","duo","excessive","devon","screensaver","helena","saves","regarded","valuation","unexpected","cigarette","fog","characteristic","marion","lobby","egyptian","tunisia","metallica","outlined","consequently","headline","treating","punch","appointments","str","gotta","cowboy","narrative","bahrain","enormous","karma","consist","betty","queens","academics","pubs","quantitative","shemales","lucas","screensavers","subdivision","tribes","vip","defeat","clicks","distinction","honduras","naughty","hazards","insured","harper","livestock","mardi","exemption","tenant","sustainability","cabinets","tattoo","shake","algebra","shadows","holly","formatting","silly","nutritional","yea","mercy","hartford","freely","marcus","sunrise","wrapping","mild","fur","nicaragua","weblogs","timeline","tar","belongs","rj","readily","affiliation","soc","fence","nudist","infinite","diana","ensures","relatives","lindsay","clan","legally","shame","satisfactory","revolutionary","bracelets","sync","civilian","telephony","mesa","fatal","remedy","realtors","breathing","briefly","thickness","adjustments","graphical","genius","discussing","aerospace","fighter","meaningful","flesh","retreat","adapted","barely","wherever","estates","rug","democrat","borough","maintains","failing","shortcuts","ka","retained","voyeurweb","pamela","andrews","marble","extending","jesse","specifies","hull","logitech","surrey","briefing","belkin","dem","accreditation","wav","blackberry","highland","meditation","modular","microphone","macedonia","combining","brandon","instrumental","giants","organizing","shed","balloon","moderators","winston","memo","ham","solved","tide","kazakhstan","hawaiian","standings","partition","invisible","gratuit","consoles","funk","fbi","qatar","magnet","translations","porsche","cayman","jaguar","reel","sheer","commodity","posing","wang","kilometers","rp","bind","thanksgiving","rand","hopkins","urgent","guarantees","infants","gothic","cylinder","witch","buck","indication","eh","congratulations","tba","cohen","sie","usgs","puppy","kathy","acre","graphs","surround","cigarettes","revenge","expires","enemies","lows","controllers","aqua","chen","emma","consultancy","finances","accepts","enjoying","conventions","eva","patrol","smell","pest","hc","italiano","coordinates","rca","fp","carnival","roughly","sticker","promises","responding","reef","physically","divide","stakeholders","hydrocodone","gst","consecutive","cornell","satin","bon","deserve","attempting","mailto","promo","jj","representations","chan","worried","tunes","garbage","competing","combines","mas","beth","bradford","len","phrases","kai","peninsula","chelsea","boring","reynolds","dom","jill","accurately","speeches","reaches","schema","considers","sofa","catalogs","ministries","vacancies","quizzes","parliamentary","obj","prefix","lucia","savannah","barrel","typing","nerve","dans","planets","deficit","boulder","pointing","renew","coupled","viii","myanmar","metadata","harold","circuits","floppy","texture","handbags","jar","ev","somerset","incurred","acknowledge","thoroughly","antigua","nottingham","thunder","tent","caution","identifies","questionnaire","qualification","locks","modelling","namely","miniature","dept","hack","dare","euros","interstate","pirates","aerial","hawk","consequence","rebel","systematic","perceived","origins","hired","makeup","textile","lamb","madagascar","nathan","tobago","presenting","cos","troubleshooting","uzbekistan","indexes","pac","rl","erp","centuries","gl","magnitude","ui","richardson","hindu","dh","fragrances","vocabulary","licking","earthquake","vpn","fundraising","fcc","markers","weights","albania","geological","assessing","lasting","wicked","eds","introduces","kills","roommate","webcams","pushed","webmasters","ro","df","computational","acdbentity","participated","junk","handhelds","wax","lucy","answering","hans","impressed","slope","reggae","failures","poet","conspiracy","surname","theology","nails","evident","whats","rides","rehab","epic","saturn","organizer","nut","allergy","sake","twisted","combinations","preceding","merit","enzyme","cumulative","zshops","planes","edmonton","tackle","disks","condo","pokemon","amplifier","ambien","arbitrary","prominent","retrieve","lexington","vernon","sans","worldcat","titanium","irs","fairy","builds","contacted","shaft","lean","bye","cdt","recorders","occasional","leslie","casio","deutsche","ana","postings","innovations","kitty","postcards","dude","drain","monte","fires","algeria","blessed","luis","reviewing","cardiff","cornwall","favors","potato","panic","explicitly","sticks","leone","transsexual","ez","citizenship","excuse","reforms","basement","onion","strand","pf","sandwich","uw","lawsuit","alto","informative","girlfriend","bloomberg","cheque","hierarchy","influenced","banners","reject","eau","abandoned","bd","circles","italic","beats","merry","mil","scuba","gore","complement","cult","dash","passive","mauritius","valued","cage","checklist","bangbus","requesting","courage","verde","lauderdale","scenarios","gazette","hitachi","divx","extraction","batman","elevation","hearings","coleman","hugh","lap","utilization","beverages","calibration","jake","eval","efficiently","anaheim","ping","textbook","dried","entertaining","prerequisite","luther","frontier","settle","stopping","refugees","knights","hypothesis","palmer","medicines","flux","derby","sao","peaceful","altered","pontiac","regression","doctrine","scenic","trainers","muze","enhancements","renewable","intersection","passwords","sewing","consistency","collectors","conclude","recognised","munich","oman","celebs","gmc","propose","hh","azerbaijan","lighter","rage","adsl","uh","prix","astrology","advisors","pavilion","tactics","trusts","occurring","supplemental","travelling","talented","annie","pillow","induction","derek","precisely","shorter","harley","spreading","provinces","relying","finals","paraguay","steal","parcel","refined","fd","bo","fifteen","widespread","incidence","fears","predict","boutique","acrylic","rolled","tuner","avon","incidents","peterson","rays","asn","shannon","toddler","enhancing","flavor","alike","walt","homeless","horrible","hungry","metallic","acne","blocked","interference","warriors","palestine","listprice","libs","undo","cadillac","atmospheric","malawi","wm","pk","sagem","knowledgestorm","dana","halo","ppm","curtis","parental","referenced","strikes","lesser","publicity","marathon","ant","proposition","gays","pressing","gasoline","apt","dressed","scout","belfast","exec","dealt","niagara","inf","eos","warcraft","charms","catalyst","trader","bucks","allowance","vcr","denial","uri","designation","thrown","prepaid","raises","gem","duplicate","electro","criterion","badge","wrist","civilization","analyzed","vietnamese","heath","tremendous","ballot","lexus","varying","remedies","validity","trustee","maui","handjobs","weighted","angola","squirt","performs","plastics","realm","corrected","jenny","helmet","salaries","postcard","elephant","yemen","encountered","tsunami","scholar","nickel","internationally","surrounded","psi","buses","expedia","geology","pct","wb","creatures","coating","commented","wallet","cleared","smilies","vids","accomplish","boating","drainage","shakira","corners","broader","vegetarian","rouge","yeast","yale","newfoundland","sn","qld","pas","clearing","investigated","dk","ambassador","coated","intend","stephanie","contacting","vegetation","doom","findarticles","louise","kenny","specially","owen","routines","hitting","yukon","beings","bite","issn","aquatic","reliance","habits","striking","myth","infectious","podcasts","singh","gig","gilbert","sas","ferrari","continuity","brook","fu","outputs","phenomenon","ensemble","insulin","assured","biblical","weed","conscious","accent","mysimon","eleven","wives","ambient","utilize","mileage","oecd","prostate","adaptor","auburn","unlock","hyundai","pledge","vampire","angela","relates","nitrogen","xerox","dice","merger","softball","referrals","quad","dock","differently","firewire","mods","nextel","framing","organised","musician","blocking","rwanda","sorts","integrating","vsnet","limiting","dispatch","revisions","papua","restored","hint","armor","riders","chargers","remark","dozens","varies","msie","reasoning","wn","liz","rendered","picking","charitable","guards","annotated","ccd","sv","convinced","openings","buys","burlington","replacing","researcher","watershed","councils","occupations","acknowledged","nudity","kruger","pockets","granny","pork","zu","equilibrium","viral","inquire","pipes","characterized","laden","aruba","cottages","realtor","merge","privilege","edgar","develops","qualifying","chassis","dubai","estimation","barn","pushing","llp","fleece","pediatric","boc","fare","dg","asus","pierce","allan","dressing","techrepublic","sperm","vg","bald","filme","craps","fuji","frost","leon","institutes","mold","dame","fo","sally","yacht","tracy","prefers","drilling","brochures","herb","tmp","alot","ate","breach","whale","traveller","appropriations","suspected","tomatoes","benchmark","beginners","instructors","highlighted","bedford","stationery","idle","mustang","unauthorized","clusters","antibody","competent","momentum","fin","wiring","io","pastor","mud","calvin","uni","shark","contributor","demonstrates","phases","grateful","emerald","gradually","laughing","grows","cliff","desirable","tract","ul","ballet","ol","journalist","abraham","js","bumper","afterwards","webpage","religions","garlic","hostels","shine","senegal","explosion","pn","banned","wendy","briefs","signatures","diffs","cove","mumbai","ozone","disciplines","casa","mu","daughters","conversations","radios","tariff","nvidia","opponent","pasta","simplified","muscles","serum","wrapped","swift","motherboard","runtime","inbox","focal","bibliographic","vagina","eden","distant","incl","champagne","ala","decimal","hq","deviation","superintendent","propecia","dip","nbc","samba","hostel","housewives","employ","mongolia","penguin","magical","influences","inspections","irrigation","miracle","manually","reprint","reid","wt","hydraulic","centered","robertson","flex","yearly","penetration","wound","belle","rosa","conviction","hash","omissions","writings","hamburg","lazy","mv","mpg","retrieval","qualities","cindy","lolita","fathers","carb","charging","cas","marvel","lined","cio","dow","prototype","importantly","rb","petite","apparatus","upc","terrain","dui","pens","explaining","yen","strips","gossip","rangers","nomination","empirical","mh","rotary","worm","dependence","discrete","beginner","boxed","lid","sexuality","polyester","cubic","deaf","commitments","suggesting","sapphire","kinase","skirts","mats","remainder","crawford","labeled","privileges","televisions","specializing","marking","commodities","pvc","serbia","sheriff","griffin","declined","guyana","spies","blah","mime","neighbor","motorcycles","elect","highways","thinkpad","concentrate","intimate","reproductive","preston","deadly","cunt","feof","bunny","chevy","molecules","rounds","longest","refrigerator","tions","intervals","sentences","dentists","usda","exclusion","workstation","holocaust","keen","flyer","peas","dosage","receivers","urls","customise","disposition","variance","navigator","investigators","cameroon","baking","marijuana","adaptive","computed","needle","baths","enb","gg","cathedral","brakes","og","nirvana","ko","fairfield","owns","til","invision","sticky","destiny","generous","madness","emacs","climb","blowing","fascinating","landscapes","heated","lafayette","jackie","wto","computation","hay","cardiovascular","ww","sparc","cardiac","salvation","dover","adrian","predictions","accompanying","vatican","brutal","learners","gd","selective","arbitration","configuring","token","editorials","zinc","sacrifice","seekers","guru","isa","removable","convergence","yields","gibraltar","levy","suited","numeric","anthropology","skating","kinda","aberdeen","emperor","grad","malpractice","dylan","bras","belts","blacks","educated","rebates","reporters","burke","proudly","pix","necessity","rendering","mic","inserted","pulling","basename","kyle","obesity","curves","suburban","touring","clara","vertex","bw","hepatitis","nationally","tomato","andorra","waterproof","expired","mj","travels","flush","waiver","pale","specialties","hayes","humanitarian","invitations","functioning","delight","survivor","garcia","cingular","economies","alexandria","bacterial","moses","counted","undertake","declare","continuously","johns","valves","gaps","impaired","achievements","donors","tear","jewel","teddy","lf","convertible","ata","teaches","ventures","nil","bufing","stranger","tragedy","julian","nest","pam","dryer","painful","velvet","tribunal","ruled","nato","pensions","prayers","funky","secretariat","nowhere","cop","paragraphs","gale","joins","adolescent","nominations","wesley","dim","lately","cancelled","scary","mattress","mpegs","brunei","likewise","banana","introductory","slovak","cakes","stan","reservoir","occurrence","idol","bloody","mixer","remind","wc","worcester","sbjct","demographic","charming","mai","tooth","disciplinary","annoying","respected","stays","disclose","affair","drove","washer","upset","restrict","springer","beside","mines","portraits","rebound","logan","mentor","interpreted","evaluations","fought","baghdad","elimination","metres","hypothetical","immigrants","complimentary","helicopter","pencil","freeze","hk","performer","abu","titled","commissions","sphere","powerseller","moss","ratios","concord","graduated","endorsed","ty","surprising","walnut","lance","ladder","italia","unnecessary","dramatically","liberia","sherman","cork","maximize","cj","hansen","senators","workout","mali","yugoslavia","bleeding","characterization","colon","likelihood","lanes","purse","fundamentals","contamination","mtv","endangered","compromise","masturbation","optimize","stating","dome","caroline","leu","expiration","namespace","align","peripheral","bless","engaging","negotiation","crest","opponents","triumph","nominated","confidentiality","electoral","changelog","welding","orgasm","deferred","alternatively","heel","alloy","condos","plots","polished","yang","gently","greensboro","tulsa","locking","casey","controversial","draws","fridge","blanket","bloom","qc","simpsons","lou","elliott","recovered","fraser","justify","upgrading","blades","pgp","loops","surge","frontpage","trauma","aw","tahoe","advert","possess","demanding","defensive","sip","flashers","subaru","forbidden","tf","vanilla","programmers","pj","monitored","installations","deutschland","picnic","souls","arrivals","spank","cw","practitioner","motivated","wr","dumb","smithsonian","hollow","vault","securely","examining","fioricet","groove","revelation","rg","pursuit","delegation","wires","bl","dictionaries","mails","backing","greenhouse","sleeps","vc","blake","transparency","dee","travis","wx","endless","figured","orbit","currencies","niger","bacon","survivors","positioning","heater","colony","cannon","circus","promoted","forbes","mae","moldova","mel","descending","paxil","spine","trout","enclosed","feat","temporarily","ntsc","cooked","thriller","transmit","apnic","fatty","gerald","pressed","frequencies","scanned","reflections","hunger","mariah","sic","municipality","usps","joyce","detective","surgeon","cement","experiencing","fireplace","endorsement","bg","planners","disputes","textiles","missile","intranet","closes","seq","psychiatry","persistent","deborah","conf","marco","assists","summaries","glow","gabriel","auditor","wma","aquarium","violin","prophet","cir","bracket","looksmart","isaac","oxide","oaks","magnificent","erik","colleague","naples","promptly","modems","adaptation","hu","harmful","paintball","prozac","sexually","enclosure","acm","dividend","newark","kw","paso","glucose","phantom","norm","playback","supervisors","westminster","turtle","ips","distances","absorption","treasures","dsc","warned","neural","ware","fossil","mia","hometown","badly","transcripts","apollo","wan","disappointed","persian","continually","communist","collectible","handmade","greene","entrepreneurs","robots","grenada","creations","jade","scoop","acquisitions","foul","keno","gtk","earning","mailman","sanyo","nested","biodiversity","excitement","somalia","movers","verbal","blink","presently","seas","carlo","workflow","mysterious","novelty","bryant","tiles","voyuer","librarian","subsidiaries","switched","stockholm","tamil","garmin","ru","pose","fuzzy","indonesian","grams","therapist","richards","mrna","budgets","toolkit","promising","relaxation","goat","render","carmen","ira","sen","thereafter","hardwood","erotica","temporal","sail","forge","commissioners","dense","dts","brave","forwarding","qt","awful","nightmare","airplane","reductions","southampton","istanbul","impose","organisms","sega","telescope","viewers","asbestos","portsmouth","cdna","meyer","enters","pod","savage","advancement","wu","harassment","willow","resumes","bolt","gage","throwing","existed","whore","generators","lu","wagon","barbie","dat","favour","soa","knock","urge","smtp","generates","potatoes","thorough","replication","inexpensive","kurt","receptors","peers","roland","optimum","neon","interventions","quilt","huntington","creature","ours","mounts","syracuse","internship","lone","refresh","aluminium","snowboard","beastality","webcast","michel","evanescence","subtle","coordinated","notre","shipments","maldives","stripes","firmware","antarctica","cope","shepherd","lm","canberra","cradle","chancellor","mambo","lime","kirk","flour","controversy","legendary","bool","sympathy","choir","avoiding","beautifully","blond","expects","cho","jumping","fabrics","antibodies","polymer","hygiene","wit","poultry","virtue","burst","examinations","surgeons","bouquet","immunology","promotes","mandate","wiley","departmental","bbs","spas","ind","corpus","johnston","terminology","gentleman","fibre","reproduce","convicted","shades","jets","indices","roommates","adware","qui","intl","threatening","spokesman","zoloft","activists","frankfurt","prisoner","daisy","halifax","encourages","ultram","cursor","assembled","earliest","donated","stuffed","restructuring","insects","terminals","crude","morrison","maiden","simulations","cz","sufficiently","examines","viking","myrtle","bored","cleanup","yarn","knit","conditional","mug","crossword","bother","budapest","conceptual","knitting","attacked","hl","bhutan","liechtenstein","mating","compute","redhead","arrives","translator","automobiles","tractor","allah","continent","ob","unwrap","fares","longitude","resist","challenged","telecharger","hoped","pike","safer","insertion","instrumentation","ids","hugo","wagner","constraint","groundwater","touched","strengthening","cologne","gzip","wishing","ranger","smallest","insulation","newman","marsh","ricky","ctrl","scared","theta","infringement","bent","laos","subjective","monsters","asylum","lightbox","robbie","stake","cocktail","outlets","swaziland","varieties","arbor","mediawiki","configurations","poison"]} diff --git a/legacy/conf/templates/config.json b/legacy/conf/templates/config.json new file mode 100644 index 0000000..1a47ad9 --- /dev/null +++ b/legacy/conf/templates/config.json @@ -0,0 +1,70 @@ +{ + "Listener": { + "Port": 13867, + "Address": "127.0.0.1", + "UseSSL": true + }, + "RelayAPI": { + "Enabled": true, + "Port": 13868, + "Address": "127.0.0.1", + "UseSSL": true + }, + "API": { + "Enabled": true, + "Port": 13869, + "Address": "127.0.0.1" + }, + "Key": "key.pem", + "Certificate": "cert.pem", + "RedisSocket": "/var/run/redis/redis.sock", + "RedisDBEphemeral": 2, + "RedisDBPersistent": 3, + "UsePassword": false, + "ConnectOnCreate": false, + "AutoReg": false, + "Debug": false, + "Trace": false, + "Relay": { + "Host": "127.0.0.1", + "Port": "2001", + "User": "x", + "Password": "x" + }, + "Logstash": { + "Host": "127.0.0.1", + "Port": "4000" + }, + "ChanKeep": { + "Enabled": false, + "MaxRelay": 30, + "SigSwitch": 20 + }, + "Dist": { + "Enabled": true, + "SendOutput": false, + "File": "conf/dist.sh" + }, + "Toggles": { + "Who": true, + "CycleChans": true + }, + "Password": "x", + "Tweaks": { + "MaxHash": 10, + "DedupPrecision": 9, + "ZNC": { + "Prefix": "*" + }, + "Delays": { + "WhoDelay": 3600, + "WhoLoop": 600, + "WhoRange": 1800, + "Timeout": 30, + "MaxDelay": 360, + "InitialDelay": 1.0, + "Factor": 2.718281828459045, + "Jitter": 0.11962656472 + } + } +} diff --git a/legacy/conf/templates/help.json b/legacy/conf/templates/help.json new file mode 100644 index 0000000..3f79c9a --- /dev/null +++ b/legacy/conf/templates/help.json @@ -0,0 +1,38 @@ +{ + "pass": "pass ", + "logout": "logout", + "mod": "mod [] []", + "who": "who ", + "join": "join []", + "part": "part ", + "enable": "enable ", + "disable": "disable ", + "stats": "stats []", + "save": "save <(file)|list|all>", + "load": "load <(file)|list|all>", + "dist": "dist", + "loadmod": "loadmod ", + "msg": "msg ", + "chans": "chans [ ...]", + "users": "users [ ...]", + "relay": "relay [] []", + "network": "network [
]", + "alias": "alias [] []", + "auto": "auto []", + "cmd": "cmd ", + "token": "token [] [relay|api]", + "all": "all ", + "allc": "allc <(network)|(alias)> ", + "admall": "admall ", + "swho": "swho []", + "list": "list []", + "exec": "exec ", + "reg": "reg []", + "confirm": "confirm ", + "pending": "pending []", + "authcheck": "authcheck []", + "recheckauth": "recheckauth []", + "blacklist": "blacklist ", + "email": "email [(domain)|] []", + "getstr": "getstr " +} diff --git a/legacy/conf/templates/irc.json b/legacy/conf/templates/irc.json new file mode 100644 index 0000000..6ec2722 --- /dev/null +++ b/legacy/conf/templates/irc.json @@ -0,0 +1,34 @@ +{ + "_": { + "register": true, + "entity": "NickServ", + "domains": [], + "registermsg": "REGISTER {password} {email}", + "identifymsg": "IDENTIFY {password}", + "confirm": "CONFIRM {token}", + "check": true, + "ping": true, + "negative": true, + "pingmsg": "STATUS", + "negativemsg": "INFO {nickname}", + "checktype": "mode", + "checkmode": "R", + "checkmsg": "Password accepted - you are now recognized.", + "checkmsg2": "You are logged in as", + "checknegativemsg": "has \u0002NOT COMPLETED\u0002 registration verification", + "checkendnegative": "End of Info" + }, + "freenode": { + "confirm": "VERIFY REGISTER {nickname} {token}" + }, + "libera": { + "confirm": "VERIFY REGISTER {nickname} {token}" + }, + "cyberia": { + "setmode": "I" + }, + "ircnow": { + "negative": false, + "ping": false + } +} diff --git a/legacy/core/bot.py b/legacy/core/bot.py new file mode 100644 index 0000000..d4b095d --- /dev/null +++ b/legacy/core/bot.py @@ -0,0 +1,871 @@ +import sys +from copy import deepcopy +from datetime import datetime +from random import randint + +from twisted.internet import reactor +from twisted.internet.defer import Deferred +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet.task import LoopingCall +from twisted.words.protocols.irc import ( + IRCBadMessage, + IRCClient, + lowDequote, + numeric_to_symbolic, +) + +import main +from core.relay import sendRelayNotification +from modules import chankeep, counters, helpers, monitor, regproc, userinfo +from utils.dedup import dedup +from utils.logging.debug import debug, trace +from utils.logging.log import error, log, warn +from utils.logging.send import sendAll +from utils.parsing import parsen + + +# Copied from the Twisted source so we can fix a bug +def parsemsg(s): + """ + Breaks a message from an IRC server into its prefix, command, and + arguments. + @param s: The message to break. + @type s: L{bytes} + @return: A tuple of (prefix, command, args). + @rtype: L{tuple} + """ + prefix = "" + trailing = [] + if not s: + raise IRCBadMessage("Empty line.") + if s[0:1] == ":": + prefix, s = s[1:].split(" ", 1) + if s.find(" :") != -1: + s, trailing = s.split(" :", 1) + args = s.split(" ") # Twisted bug fixed by adding an argument to split() + args.append(trailing) + else: + args = s.split(" ") # And again + command = args.pop(0) + return prefix, command, args + + +class IRCBot(IRCClient): + def __init__(self, net, num): + self.isconnected = False + self.channels = [] + self.net = net + self.authenticated = not regproc.needToAuth(self.net) + self.num = num + self.buffer = "" + self.name = net + str(num) + alias = main.alias[num] + relay = main.network[self.net].relays[num] + self.netinst = main.network[self.net] + self.nickname = alias["nick"] + self.realname = alias["realname"] + self.username = alias["nick"].lower() + "/" + relay["net"] + self.password = main.config["Relay"]["Password"] + self.userinfo = None # + self.fingerReply = None # + self.versionName = None # Don't give out information + self.versionNum = None # + self.versionEnv = None # + self.sourceURL = None # + + self._getWho = {} # LoopingCall objects -- needed to be able to stop them + + self._tempWho = {} # temporary storage for gathering WHO info + self._tempNames = {} # temporary storage for gathering NAMES info + self._tempList = ([], []) # temporary storage for gathering LIST info + self.listOngoing = False # we are currently receiving a LIST + self.listRetried = False # we asked and got nothing so asked again + self.listAttempted = False # we asked for a list + self.listSimple = False # after asking again we got the list, so use the simple + # syntax from now on + self.wantList = False # we want to send a LIST, but not all relays are active yet + self.chanlimit = 0 + self.prefix = {} + self.servername = None + + self._regAttempt = None + self._negativePass = None + + def lineReceived(self, line): + if bytes != str and isinstance(line, bytes): + # decode bytes from transport to unicode + line = line.decode("utf-8", "replace") + + line = lowDequote(line) + try: + prefix, command, params = parsemsg(line) + if command in numeric_to_symbolic: + command = numeric_to_symbolic[command] + try: + self.handleCommand(command, prefix, params) + except Exception as err: + error(err) + except IRCBadMessage: + self.badMessage(line, *sys.exc_info()) + + def joinChannels(self, channels): + sleeptime = 0.0 + increment = 0.8 + for i in channels: + if i not in self.channels: + if self.net in main.blacklist.keys(): + if i in main.blacklist[self.net]: + debug("Not joining blacklisted channel %s on %s - %i" % (i, self.net, self.num)) + continue + trace(self.net, "-", self.num, ": joining", i, "in", sleeptime, "seconds") + reactor.callLater(sleeptime, self.join, i) + sleeptime += increment + if sleeptime == 10: + sleeptime = 0.0 + increment = 0.7 + increment += 0.1 + else: + error("%s - Cannot join channel we are already on: %s - %i" % (i, self.net, self.num)) + + def checkChannels(self): + if chankeep.allRelaysActive(self.net): + debug(f"checkChannels() all relays active for {self.net}") + else: + debug("checkChannels() skipping channel check as we have inactive relays: %s - %i" % (self.net, self.num)) + return + if self.net in main.TempChan.keys(): + if self.num in main.TempChan[self.net].keys(): + self.joinChannels(main.TempChan[self.net][self.num]) + del main.TempChan[self.net][self.num] + if not main.TempChan[self.net]: + del main.TempChan[self.net] + + def event(self, **cast): + if "ts" not in cast.keys(): + cast["ts"] = str(datetime.now().isoformat()) + + # remove odd stuff + for i in list(cast.keys()): # Make a copy of the .keys() as Python 3 cannot handle iterating over + if cast[i] == "": # a dictionary that changes length with each iteration + del cast[i] + # remove server stuff + if "muser" in cast.keys(): + if cast["muser"] == self.servername: + cast["type"] = "conn" + if "channel" in cast.keys(): + if cast["channel"] == "*": + cast["type"] = "conn" + ## + + # expand out the hostmask + if not {"nick", "ident", "host"}.issubset(set(cast.keys())): + if "muser" in cast.keys(): + cast["nick"], cast["ident"], cast["host"] = parsen(cast["muser"]) + + # handle ZNC stuff + if {"nick", "ident", "host", "msg"}.issubset(set(cast)): + if cast["ident"] == "znc" and cast["host"] == "znc.in": + cast["type"] = "znc" + cast["num"] = self.num + del cast["nick"] + del cast["ident"] + del cast["host"] + del cast["channel"] + del cast["muser"] + if "Disconnected from IRC" in cast["msg"]: + log("ZNC disconnected on %s - %i" % (self.net, self.num)) + self.isconnected = False + self.authenticated = False + if "Connected!" in cast["msg"]: + log("ZNC connected on %s - %i" % (self.net, self.num)) + self.isconnected = True + if "could not be joined, disabling it" in cast["msg"]: + error(cast["msg"]) + # + + # don't reprocess the same message twice + # if the type is in that list, it's already been here, don't run it again + if not cast["type"] in {"query", "self", "highlight", "znc", "who", "conn"}: + cast["num"] = self.num + if "channel" in cast.keys(): + if cast["type"] == "mode": + if cast["channel"].lower() == self.nickname.lower(): + # castDup = deepcopy(cast) + cast["mtype"] = cast["type"] + cast["type"] = "self" + # self.event(**castDup) + if cast["modearg"]: # check if modearg is non-NoneType + if self.nickname.lower() == cast["modearg"].lower(): + castDup = deepcopy(cast) + castDup["mtype"] = cast["type"] + castDup["type"] = "highlight" + self.event(**castDup) + else: + if cast["channel"].lower() == self.nickname.lower() or not cast["channel"].startswith("#"): + cast["mtype"] = cast["type"] + cast["type"] = "query" + # self.event(**castDup) + # Don't call self.event for this one because queries are not events on a + # channel, but we still want to see them + if cast["channel"] == "AUTH": + cast["type"] = "conn" + cast["mtype"] = cast["type"] + + # TODO: better way to do this + # as we changed the types above, check again + if not cast["type"] in {"query", "self", "highlight", "znc", "who", "conn"}: + # we have been kicked + if "user" in cast.keys(): + if cast["user"].lower() == self.nickname.lower(): + castDup = deepcopy(cast) + castDup["mtype"] = cast["type"] + castDup["type"] = "self" + if self.net in main.blacklist.keys(): + if cast["channel"] not in main.blacklist[self.net]: + main.blacklist[self.net].append(cast["channel"]) + else: + main.blacklist[self.net] = [cast["channel"]] + main.saveConf("blacklist") + self.event(**castDup) + + # we sent a message/left/joined/kick someone/quit + if "nick" in cast.keys(): + if cast["nick"].lower() == self.nickname.lower(): + castDup = deepcopy(cast) + castDup["mtype"] = cast["type"] + castDup["type"] = "self" + + # we have been mentioned in a msg/notice/action/part/quit/topic message + if "msg" in cast.keys(): # Don't highlight queries + if cast["msg"] is not None: + if self.nickname.lower() in cast["msg"].lower(): + castDup = deepcopy(cast) + castDup["mtype"] = cast["type"] + castDup["type"] = "highlight" + self.event(**castDup) + + if "net" not in cast.keys(): + cast["net"] = self.net + if "num" not in cast.keys(): + cast["num"] = self.num + if not self.authenticated: + regproc.registerTest(cast) + counters.event(self.net, cast["type"]) + monitor.event(self.net, cast) + + def privmsg(self, user, channel, msg): + self.event(type="msg", muser=user, channel=channel, msg=msg) + + def noticed(self, user, channel, msg): + self.event(type="notice", muser=user, channel=channel, msg=msg) + + def action(self, user, channel, msg): + self.event(type="action", muser=user, channel=channel, msg=msg) + + def sendmsg(self, channel, msg, in_query=False): + query = f"{self.nickname}!*@*" + res = userinfo.getWhoSingle(self.net, query) + if not res: + res = [] + us = list(res) + if len(us) > 0: + hostmask = us[0] + else: + # Close enough... + hostmask = f"{self.nickname}!{self.nickname}@{self.servername}" + warn(f"Could not get a hostname, using {hostmask}") + nick, ident, host = parsen(hostmask) + # We sent someone a query reply + if in_query: + self.event(type="self", mtype="msg", channel=self.nickname, nick=channel, ident=ident, host=host, msg=msg) + else: + self.event(type="self", mtype="msg", channel=channel, nick=self.nickname, ident=ident, host=host, msg=msg) + self.event(type="msg", channel=channel, nick=self.nickname, ident=ident, host=host, msg=msg) + self.msg(channel, msg) + + def get(self, var): + try: + result = getattr(self, var) + except AttributeError: + result = None + return result + + def setNick(self, nickname): + self._attemptedNick = nickname + self.sendLine("NICK %s" % nickname) + self.nickname = nickname + + def alterCollidedNick(self, nickname): + newnick = nickname + "_" + return newnick + + def nickChanged(self, olduser, newnick): + self.nickname = newnick + self.event(type="self", mtype="nick", muser=olduser, user=newnick) + + def irc_ERR_NICKNAMEINUSE(self, prefix, params): + self._attemptedNick = self.alterCollidedNick(self._attemptedNick) + self.setNick(self._attemptedNick) + + def irc_ERR_PASSWDMISMATCH(self, prefix, params): + log("%s - %i: password mismatch as %s" % (self.net, self.num, self.username)) + sendAll("%s - %i: password mismatch as %s" % (self.net, self.num, self.username)) + + def _who(self, channel): + d = Deferred() + if channel not in self._tempWho: + self._tempWho[channel] = ([], []) + self._tempWho[channel][0].append(d) + self.sendLine("WHO %s" % channel) + return d + + def who(self, channel): + self._who(channel).addCallback(self.got_who) + + def irc_RPL_WHOREPLY(self, prefix, params): + channel = params[1] + ident = params[2] + host = params[3] + server = params[4] + nick = params[5] + status = params[6] + realname = params[7] + if channel not in self._tempWho: + return + n = self._tempWho[channel][1] + n.append([nick, nick, host, server, status, realname]) + self.event( + type="who", + nick=nick, + ident=ident, + host=host, + realname=realname, + channel=channel, + server=server, + status=status, + ) + + def irc_RPL_ENDOFWHO(self, prefix, params): + channel = params[1] + if channel not in self._tempWho: + return + callbacks, info = self._tempWho[channel] + for cb in callbacks: + cb.callback((channel, info)) + del self._tempWho[channel] + + def got_who(self, whoinfo): + userinfo.initialUsers(self.net, whoinfo[0], whoinfo[1]) + + def sanit(self, data): + if len(data) >= 1: + if data[0] in self.prefix.keys(): + return ( + self.prefix[data[0]], + data[1:], + ) # would use a set but it's possible these are the same + return (None, data) + else: + return (None, False) + + def names(self, channel): + d = Deferred() + if channel not in self._tempNames: + self._tempNames[channel] = ([], []) + self._tempNames[channel][0].append(d) + self.sendLine("NAMES %s" % channel) + return d + + def irc_RPL_NAMREPLY(self, prefix, params): + channel = params[2] + nicklist = params[3].split(" ") + if channel not in self._tempNames: + return + n = self._tempNames[channel][1] + n.append(nicklist) + + def irc_RPL_ENDOFNAMES(self, prefix, params): + channel = params[1] + if channel not in self._tempNames: + return + callbacks, namelist = self._tempNames[channel] + for cb in callbacks: + cb.callback((channel, namelist)) + del self._tempNames[channel] + + def got_names(self, nicklist): + newNicklist = [] + for i in nicklist[1]: + for x in i: + mode, nick = self.sanit(x) + if nick: + newNicklist.append((mode, nick)) + userinfo.initialNames(self.net, nicklist[0], newNicklist) + + def myInfo(self, servername, version, umodes, cmodes): + self.servername = servername + + def _list(self, noargs): + d = Deferred() + self._tempList = ([], []) + self._tempList[0].append(d) + if self.listSimple: + self.sendLine("LIST") + return d # return early if we know what to do + + if noargs: + self.sendLine("LIST") + else: + self.sendLine("LIST >0") + return d + + def list(self, noargs=False, nocheck=False): + if not self.authenticated: + debug("Will not send LIST, unauthenticated: %s - %i" % (self.net, self.num)) + return + if self.listAttempted: + debug("List request dropped, already asked for LIST - %s - %i" % (self.net, self.num)) + return + else: + self.listAttempted = True + if self.listOngoing: + debug("LIST request dropped, already ongoing - %s - %i" % (self.net, self.num)) + return + else: + if nocheck: + allRelays = True # override the system - if this is + else: # specified, we already did this + allRelays = chankeep.allRelaysActive(self.net) + if not allRelays: + self.wantList = True + debug("Not all relays were active for LIST request") + return + self._list(noargs).addCallback(self.got_list) + + def irc_RPL_LISTSTART(self, prefix, params): + self.listAttempted = False + self.listOngoing = True + self.wantList = False + + def irc_RPL_LIST(self, prefix, params): + channel = params[1] + users = params[2] + topic = params[3] + self._tempList[1].append([channel, users, topic]) + + def irc_RPL_LISTEND(self, prefix, params): + if not len(self._tempList[0]) > 0: # there are no callbacks, can't do anything there + debug("We didn't ask for this LIST, discarding") + self._tempList[0].clear() + self._tempList[1].clear() + return + callbacks, info = self._tempList + self.listOngoing = False + for cb in callbacks: + cb.callback((info)) + noResults = False + if len(self._tempList[1]) == 0: + noResults = True + self._tempList[0].clear() + self._tempList[1].clear() + if noResults: + if self.listRetried: + warn("LIST still empty after retry: %s - %i" % (self.net, self.num)) + self.listRetried = False + return + else: + self.list(True) + self.listRetried = True + else: + if self.listRetried: + self.listRetried = False + debug("List received after retry - defaulting to simple list syntax: %s - %i" % (self.net, self.num)) + self.listSimple = True + + def got_list(self, listinfo): + if len(listinfo) == 0: # probably ngircd not supporting LIST >0 + return + if main.config["ChanKeep"]["Enabled"]: + chankeep.initialList(self.net, self.num, listinfo) + + def recheckList(self): + if not main.config["ChanKeep"]["Enabled"]: + return + allRelays = chankeep.allRelaysActive(self.net) + debug(f"recheckList() all relays for {self.net} {allRelays}") + if allRelays: + debug(f"recheckList() all relays active for {self.net}") + first_relay = helpers.get_first_relay(self.net) + debug(f"recheckList() first relay for {self.net}: {first_relay.num}") + if first_relay: + if first_relay.wantList is True: + first_relay.list(nocheck=True) + debug(f"recheckList() asking for a list for {self.net} after final relay {self.num} connected") + else: + debug(f"recheckList() first relay wantList is False for {self.net} ({first_relay.num})") + # name = self.net + "1" + + # if self.num == 1: # Only one instance should do a list + if helpers.is_first_relay(self.net, self.num): + debug(f"recheckList() we are the first relay for {self.net} ({self.num})") + if self.chanlimit: + if allRelays: + self.list() + debug(f"recheckList() requested a list for {self.net} from {self.num}") + else: + debug(f"recheckList() not all relays active for {self.net}") + self.wantList = True + else: + debug("recheckList() aborting LIST due to bad chanlimit") + self.checkChannels() + + def seed_chanlimit(self, chanlimit): + if not main.network[self.net].relays[self.num][ + "registered" + ]: # TODO: add check for register request sent, only send it once + if main.config["AutoReg"]: + if not self.authenticated: + self._regAttempt = reactor.callLater(5, regproc.registerAccount, self.net, self.num) + # regproc.registerAccount(self.net, self.num) + try: + self.chanlimit = int(chanlimit) + except TypeError: + warn("Invalid chanlimit: %s" % chanlimit) + if self.chanlimit == 0: + self.chanlimit = 200 # don't take the piss if it's not limited + net_inst_chanlimit = self.netinst.chanlimit + if net_inst_chanlimit: + if self.chanlimit > net_inst_chanlimit: + self.chanlimit = net_inst_chanlimit + # warn(f"Chanlimit on {self.net} too high, setting to {self.chanlimit}") + + if not regproc.needToRegister(self.net): # if we need to register, only recheck on auth confirmation + if main.config["ChanKeep"]["Enabled"]: + self.recheckList() + + def seed_prefix(self, prefix): + prefix = prefix.replace(")", "") + prefix = prefix.replace("(", "") + length = len(prefix) + half = int(length / 2) + prefixToMode = dict(zip(prefix[half:], prefix[:half])) + self.prefix = prefixToMode + + def isupport(self, options): + interested = {"CHANLIMIT", "MAXCHANNELS", "PREFIX"} + newOptions = {x for x in options if any(y in x for y in interested)} + if len(newOptions) == 0: + return + if not self.isconnected: + log("endpoint connected: %s - %i" % (self.net, self.num)) + self.isconnected = True + for i in newOptions: + if i.startswith("PREFIX"): + if "=" in i: + split = i.split("=") + if len(split) == 2: + prefix = split[1] + self.seed_prefix(prefix) + elif i.startswith("CHANLIMIT"): + if ":" in i: + split = i.split(":") + if len(split) >= 2: + chanlimit = split[1] + self.seed_chanlimit(chanlimit) + elif i.startswith("MAXCHANNELS"): + if "=" in i: + split = i.split("=") + if len(split) == 2: + chanlimit = split[1] + self.seed_chanlimit(chanlimit) + + # We need to override these functions as Twisted discards + # the hostname and other useful information in the functions + # that these call by default + def irc_JOIN(self, prefix, params): + nick = prefix.split("!")[0] + channel = params[-1] + if nick == self.nickname: + self.joined(channel) + else: + self.userJoined(prefix, channel) + + def irc_PART(self, prefix, params): + nick = prefix.split("!")[0] + channel = params[0] + if len(params) >= 2: + message = params[1] + else: + message = None + if nick == self.nickname: + self.left(prefix, channel, message) + else: + self.userLeft(prefix, channel, message) + + def irc_QUIT(self, prefix, params): + # nick = prefix.split("!")[0] + self.userQuit(prefix, params[0]) + + def irc_NICK(self, prefix, params): + nick = prefix.split("!", 1)[0] + if nick == self.nickname: + self.nickChanged(prefix, params[0]) + else: + self.userRenamed(prefix, params[0]) + + def irc_KICK(self, prefix, params): + channel = params[0] + kicked = params[1] + message = params[-1] + # Checks on whether it was us that was kicked are done in userKicked + self.userKicked(kicked, channel, prefix, message) + + def irc_TOPIC(self, prefix, params): + channel = params[0] + newtopic = params[1] + self.topicUpdated(prefix, channel, newtopic) + + # End of Twisted hackery + + def regPing(self, negativepass=None, reset=True): + if not main.config["AutoReg"]: + return + if self.authenticated: + return + if not regproc.needToAuth(self.net): + self.authenticated = True + return + sinst = regproc.substitute(self.net, self.num) + if not sinst: + error(f"regPing() {self.net}: registration ping failed for {self.num}") + return + if reset: + self._negativePass = None + + debug(f"regPing() {self.net} - {self.num}: _negativePass:{self._negativePass} negativepass:{negativepass}") + if self._negativePass is None: + # We have failed, the blacklisted message has been found + if negativepass is False: + self._negativePass = False + debug( + ( + f"regPing() {self.net} - {self.num} not passed negative:checknegativemsg " + f"check, {sinst['checknegativemsg']} present in message" + ) + ) + return + # End of negative output reached with no blacklisted message + elif negativepass is True: + if self._negativePass is None: # check if it's not failed yet + self._negativePass = True + debug( + ( + f"regPing() {self.net} - {self.num} passed negative:checkendnegative " + f"check, {sinst['checkendnegative']} present in message" + ) + ) + else: + debug(f"regPing() {self.net}: negative registration check - {self.num}") + return + else: + if sinst["negative"]: + self.msg(sinst["entity"], sinst["negativemsg"]) + debug( + ( + f"regPing() {self.net}: sent negativemsg " + f"'{sinst['negativemsg']}' to {sinst['entity']} - {self.num}" + ) + ) + return + else: + self._negativePass = True # if it's disabled, we still want the next block to run + + # Only run if negativepass has completed, or exempted as above + if self._negativePass: + if sinst["check"]: + if sinst["ping"]: + self.msg(sinst["entity"], sinst["pingmsg"]) + debug(f"regPing() {self.net}: sent ping '{sinst['pingmsg']}' to {sinst['entity']} - {self.num}") + return + else: + self.authenticated = True + + def signedOn(self): + log("signed on: %s - %i" % (self.net, self.num)) + ctime = str(datetime.now().isoformat()) + sendRelayNotification( + { + "type": "conn", + "net": self.net, + "num": self.num, + "status": "signedon", + "ts": ctime, + } + ) + if not self.authenticated: + reactor.callLater(10, self.regPing) + + def setup_who_loop(self, channel): + # if main.config["Toggles"]["Who"]: + lc = LoopingCall(self.who, channel) + self._getWho[channel] = lc + intrange = main.config["Tweaks"]["Delays"]["WhoRange"] + minint = main.config["Tweaks"]["Delays"]["WhoLoop"] + interval = randint(minint, minint + intrange) + lc.start(interval) + + def joined(self, channel): + if channel not in self.channels: + self.channels.append(channel) + self.names(channel).addCallback(self.got_names) + if main.config["Toggles"]["Who"]: + reactor.callLater(main.config["Tweaks"]["Delays"]["WhoDelay"], self.setup_who_loop, channel) + # lc = LoopingCall(self.who, channel) + # self._getWho[channel] = lc + # intrange = main.config["Tweaks"]["Delays"]["WhoRange"] + # minint = main.config["Tweaks"]["Delays"]["WhoLoop"] + # interval = randint(minint, minint + intrange) + # lc.start(interval) + + def botLeft(self, channel): + if channel in self.channels: + self.channels.remove(channel) + if channel in self._getWho.keys(): + lc = self._getWho[channel] + lc.stop() + del self._getWho[channel] + userinfo.delChannels(self.net, [channel]) # < we do not need to deduplicate this + # log("Can no longer cover %s, removing records" % channel)# as it will only be matched once -- + # other bots have different nicknames so + + def left(self, user, channel, message): # even if they saw it, they wouldn't react + self.event(type="part", muser=user, channel=channel, msg=message) + self.botLeft(channel) + + def userJoined(self, user, channel): + self.event(type="join", muser=user, channel=channel) + + def userLeft(self, user, channel, message): + self.event(type="part", muser=user, channel=channel, msg=message) + + def userQuit(self, user, quitMessage): + self.chanlessEvent({"type": "quit", "muser": user, "msg": quitMessage}) + + def userKicked(self, kickee, channel, kicker, message): + if kickee.lower() == self.nickname.lower(): + self.botLeft(channel) + self.event(type="kick", muser=kicker, channel=channel, msg=message, user=kickee) + + def chanlessEvent(self, cast): + cast["ts"] = str(datetime.now().isoformat()) + cast["nick"], cast["ident"], cast["host"] = parsen(cast["muser"]) + if dedup(self.name, cast): # Needs to be kept self.name until the dedup + # function is converted to the new net, num + # format + return # stop right there sir! + chans = userinfo.getChanList(self.net, cast["nick"]) + if chans is None: + error("No channels returned for chanless event: %s" % cast) + # self.event(**cast) -- no, should NEVER happen + return + # getChansSingle returns all channels of the user, we only want to use + # ones we have common with them + realChans = set(chans).intersection(set(self.channels)) + for i in realChans: + cast["channel"] = i + self.event(**cast) + + def userRenamed(self, oldname, newname): + self.chanlessEvent({"type": "nick", "muser": oldname, "user": newname}) + + def topicUpdated(self, user, channel, newTopic): + self.event(type="topic", muser=user, channel=channel, msg=newTopic) + + def modeChanged(self, user, channel, toset, modes, args): + argList = list(args) + modeList = [i for i in modes] + for a, m in zip(argList, modeList): + self.event( + type="mode", + muser=user, + channel=channel, + mode=m, + status=toset, + modearg=a, + ) + + +# TODO: strip out relay functionality +class IRCBotFactory(ReconnectingClientFactory): + def __init__(self, net, num=None, relayCommands=None, user=None, stage2=None): + if net is None: + self.num = num + self.net = None + self.name = "relay - %i" % num + self.relay = True + else: + self.name = net + str(num) + self.num = num + self.net = net + self.relay = False + self.client = None + self.maxDelay = main.config["Tweaks"]["Delays"]["MaxDelay"] + self.initialDelay = main.config["Tweaks"]["Delays"]["InitialDelay"] + self.factor = main.config["Tweaks"]["Delays"]["Factor"] + self.jitter = main.config["Tweaks"]["Delays"]["Jitter"] + + self.relayCommands, self.user, self.stage2 = relayCommands, user, stage2 + + def buildProtocol(self, addr): + entry = IRCBot(self.net, self.num) + main.IRCPool[self.name] = entry + + self.client = entry + return entry + + def clientConnectionLost(self, connector, reason): + if not self.relay: + userinfo.delChannels(self.net, self.client.channels) + if self.client is not None: + self.client.isconnected = False + self.client.authenticated = False + self.client.channels = [] + error = reason.getErrorMessage() + log("%s - %i: connection lost: %s" % (self.net, self.num, error)) + sendAll("%s - %i: connection lost: %s" % (self.net, self.num, error)) + ctime = str(datetime.now().isoformat()) + sendRelayNotification( + { + "type": "conn", + "net": self.net, + "num": self.num, + "status": "lost", + "message": error, + "ts": ctime, + } + ) + self.retry(connector) + # ReconnectingClientFactory.clientConnectionLost(self, connector, reason) + + def clientConnectionFailed(self, connector, reason): + if self.client is not None: + self.client.isconnected = False + self.client.authenticated = False + self.client.channels = [] + error = reason.getErrorMessage() + log("%s - %i: connection failed: %s" % (self.net, self.num, error)) + if not self.relay: + sendAll("%s - %s: connection failed: %s" % (self.net, self.num, error)) + ctime = str(datetime.now().isoformat()) + sendRelayNotification( + { + "type": "conn", + "net": self.net, + "num": self.num, + "status": "failed", + "message": error, + "ts": ctime, + } + ) + self.retry(connector) + # ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) diff --git a/legacy/core/logstash.py b/legacy/core/logstash.py new file mode 100644 index 0000000..1e3c2c9 --- /dev/null +++ b/legacy/core/logstash.py @@ -0,0 +1,28 @@ +import logging +from json import dumps + +import logstash + +import main + +logger = None + + +def init_logstash(): + global logger + logger = logging.getLogger("ingest") + logger.setLevel(logging.INFO) + logger.addHandler( + logstash.TCPLogstashHandler( + main.config["Logstash"]["Host"], + int(main.config["Logstash"]["Port"]), + version=1, + ) + ) + + +def sendLogstashNotification(text): + if logger is not None: + logger.info(dumps(text)) + return True + return False diff --git a/legacy/core/parser.py b/legacy/core/parser.py new file mode 100644 index 0000000..da0312c --- /dev/null +++ b/legacy/core/parser.py @@ -0,0 +1,25 @@ +import main +from utils.logging.log import warn +from utils.logging.send import incorrectUsage, sendFailure, sendInfo, sendSuccess + + +def parseCommand(addr, authed, data): + # call command modules with: (addr, authed, data, spl, success, failure, info, incUsage, length) + spl = data.split() + if addr in main.connections.keys(): + obj = main.connections[addr] + else: + warn("Got connection object with no instance in the address pool") + return + + success = lambda data: sendSuccess(addr, data) # noqa: E731 + failure = lambda data: sendFailure(addr, data) # noqa: E731 + info = lambda data: sendInfo(addr, data) # noqa: E731 + + incUsage = lambda mode: incorrectUsage(addr, mode) # noqa: E731 + length = len(spl) + if spl[0] in main.CommandMap.keys(): + main.CommandMap[spl[0]](addr, authed, data, obj, spl, success, failure, info, incUsage, length) + return + incUsage(None) + return diff --git a/legacy/core/relay.py b/legacy/core/relay.py new file mode 100644 index 0000000..7ae0cba --- /dev/null +++ b/legacy/core/relay.py @@ -0,0 +1,161 @@ +from json import dumps, loads + +from twisted.internet.protocol import Factory, Protocol + +import main +from utils.logging.log import log, warn + +validTypes = [ + "msg", + "notice", + "action", + "who", + "part", + "join", + "kick", + "quit", + "nick", + "topic", + "mode", + "conn", + "znc", + "query", + "self", + "highlight", +] + + +class Relay(Protocol): + def __init__(self, addr): + self.addr = addr + self.authed = False + self.subscriptions = [] + + def send(self, data): + data += "\r\n" + data = data.encode("utf-8", "replace") + self.transport.write(data) + + def sendErr(self, data): + self.send(dumps({"type": "error", "reason": data})) + return + + def sendMsg(self, data): + self.send(dumps(data)) + + def dataReceived(self, data): + data = data.decode("utf-8", "replace") + try: + parsed = loads(data) + except: # noqa: E722 + self.sendErr("MALFORMED") + return + if "type" not in parsed.keys(): + self.sendErr("NOTYPE") + return + if parsed["type"] == "hello": + if set(["key", "hello"]).issubset(set(parsed)): + self.handleHello(parsed) + else: + self.sendErr("WRONGFIELDS") + return + return + elif parsed["type"] == "control": + if "subscribe" in parsed.keys(): + if self.authed: + self.handleSubscribe(parsed["subscribe"]) + return + else: + self.sendErr("DENIED") + return + elif "unsubscribe" in parsed.keys(): + if self.authed: + self.handleUnsubscribe(parsed["unsubscribe"]) + return + else: + self.sendErr("DENIED") + return + else: + self.sendErr("UNCLEAR") + return + else: + self.sendErr("UNCLEAR") + return + + def handleSubscribe(self, lst): + if not isinstance(lst, list): + self.sendErr("NOTLIST") + return + for i in lst: + if i not in validTypes: + self.sendErr("NONEXISTANT") + return + if i in self.subscriptions: + self.sendErr("SUBSCRIBED") + return + self.subscriptions.append(i) + self.sendMsg({"type": "success"}) + return + + def handleUnubscribe(self, lst): + if not isinstance(lst, list): + self.sendErr("NOTLIST") + return + for i in lst: + if i not in validTypes: + self.sendErr("NONEXISTANT") + return + if i not in self.subscriptions: + self.sendErr("NOTSUBSCRIBED") + return + del self.subscriptions[i] + self.sendMsg({"type": "success"}) + return + + def handleHello(self, parsed): + if parsed["key"] in main.tokens.keys(): + if ( + parsed["hello"] == main.tokens[parsed["key"]]["hello"] + and main.tokens[parsed["key"]]["usage"] == "relay" + ): + self.sendMsg({"type": "hello", "hello": main.tokens[parsed["key"]]["counter"]}) + self.authed = True + else: + self.transport.loseConnection() + return + else: + self.sendErr("NOKEY") + return + + def connectionMade(self): + log("Relay connection from %s:%s" % (self.addr.host, self.addr.port)) + # self.send("Greetings.") + + def connectionLost(self, reason): + self.authed = False + log("Relay connection lost from %s:%s -- %s" % (self.addr.host, self.addr.port, reason.getErrorMessage())) + if self.addr in main.relayConnections.keys(): + del main.relayConnections[self.addr] + else: + warn("Tried to remove a non-existant relay connection.") + + +class RelayFactory(Factory): + def buildProtocol(self, addr): + entry = Relay(addr) + main.relayConnections[addr] = entry + return entry + + def send(self, addr, data): + if addr in main.relayConnections.keys(): + connection = main.relayConnections[addr] + connection.send(data) + else: + return + + +def sendRelayNotification(cast): + for i in main.relayConnections.keys(): + if main.relayConnections[i].authed: + if cast["type"] in main.relayConnections[i].subscriptions: + main.relayConnections[i].send(dumps(cast)) diff --git a/legacy/core/server.py b/legacy/core/server.py new file mode 100644 index 0000000..581d237 --- /dev/null +++ b/legacy/core/server.py @@ -0,0 +1,55 @@ +from twisted.internet.protocol import Factory, Protocol + +import main +from core.parser import parseCommand +from utils.logging.log import log, warn + + +class Server(Protocol): + def __init__(self, addr): + self.addr = addr + self.authed = False + if main.config["UsePassword"] is False: + self.authed = True + + def send(self, data): + data += "\r\n" + data = data.encode("utf-8", "replace") + self.transport.write(data) + + def dataReceived(self, data): + data = data.decode("utf-8", "replace") + # log("Data received from %s:%s -- %s" % (self.addr.host, self.addr.port, repr(data))) + if "\n" in data: + splitData = [x for x in data.split("\n") if x] + if "\n" in data: + for i in splitData: + parseCommand(self.addr, self.authed, i) + return + parseCommand(self.addr, self.authed, data) + + def connectionMade(self): + log("Connection from %s:%s" % (self.addr.host, self.addr.port)) + self.send("Greetings.") + + def connectionLost(self, reason): + self.authed = False + log("Connection lost from %s:%s -- %s" % (self.addr.host, self.addr.port, reason.getErrorMessage())) + if self.addr in main.connections.keys(): + del main.connections[self.addr] + else: + warn("Tried to remove a non-existant connection.") + + +class ServerFactory(Factory): + def buildProtocol(self, addr): + entry = Server(addr) + main.connections[addr] = entry + return entry + + def send(self, addr, data): + if addr in main.connections.keys(): + connection = main.connections[addr] + connection.send(data) + else: + return diff --git a/legacy/docker-compose.yml b/legacy/docker-compose.yml new file mode 100644 index 0000000..28d0c60 --- /dev/null +++ b/legacy/docker-compose.yml @@ -0,0 +1,41 @@ +version: "2" + +services: + app: + image: pathogen/threshold:latest + build: ./docker + volumes: + - ${PORTAINER_GIT_DIR}:/code + - ${THRESHOLD_CONFIG_DIR}:/code/conf/live + #- ${THRESHOLD_TEMPLATE_DIR}:/code/conf/templates + - ${THRESHOLD_CERT_DIR}:/code/conf/cert + ports: + - "${THRESHOLD_LISTENER_PORT}:${THRESHOLD_LISTENER_PORT}" + - "${THRESHOLD_RELAY_PORT}:${THRESHOLD_RELAY_PORT}" + - "${THRESHOLD_API_PORT}:${THRESHOLD_API_PORT}" + env_file: + - .env + # for development + extra_hosts: + - "host.docker.internal:host-gateway" + volumes_from: + - tmp + + tmp: + image: busybox + command: chmod -R 777 /var/run/redis + volumes: + - /var/run/redis + + redis: + image: redis + command: redis-server /etc/redis.conf + volumes: + - ${PORTAINER_GIT_DIR}/docker/redis.conf:/etc/redis.conf + volumes_from: + - tmp + +networks: + default: + external: + name: pathogen \ No newline at end of file diff --git a/legacy/docker/Dockerfile b/legacy/docker/Dockerfile new file mode 100644 index 0000000..f89ac37 --- /dev/null +++ b/legacy/docker/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1 +FROM python:3 + +RUN useradd -d /code pathogen +RUN mkdir /code +RUN chown pathogen:pathogen /code + +RUN mkdir /venv +RUN chown pathogen:pathogen /venv + +USER pathogen +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +WORKDIR /code +COPY requirements.prod.txt /code/ +RUN python -m venv /venv +RUN . /venv/bin/activate && pip install -r requirements.prod.txt +CMD . /venv/bin/activate && exec python /code/threshold \ No newline at end of file diff --git a/legacy/docker/docker-compose.prod.yml b/legacy/docker/docker-compose.prod.yml new file mode 100644 index 0000000..e8e011d --- /dev/null +++ b/legacy/docker/docker-compose.prod.yml @@ -0,0 +1,38 @@ +version: "2" + +services: + app: + image: pathogen/threshold:latest + build: ./docker + volumes: + - ${PORTAINER_GIT_DIR}:/code + - ${THRESHOLD_CONFIG_DIR}:/code/conf/live + #- ${THRESHOLD_TEMPLATE_DIR}:/code/conf/templates + - ${THRESHOLD_CERT_DIR}:/code/conf/cert + ports: + - "${THRESHOLD_LISTENER_PORT}:${THRESHOLD_LISTENER_PORT}" + - "${THRESHOLD_RELAY_PORT}:${THRESHOLD_RELAY_PORT}" + - "${THRESHOLD_API_PORT}:${THRESHOLD_API_PORT}" + env_file: + - ../stack.env + volumes_from: + - tmp + + tmp: + image: busybox + command: chmod -R 777 /var/run/redis + volumes: + - /var/run/redis + + redis: + image: redis + command: redis-server /etc/redis.conf + volumes: + - ${PORTAINER_GIT_DIR}/docker/redis.conf:/etc/redis.conf + volumes_from: + - tmp + +networks: + default: + external: + name: pathogen \ No newline at end of file diff --git a/legacy/docker/redis.conf b/legacy/docker/redis.conf new file mode 100644 index 0000000..46366bf --- /dev/null +++ b/legacy/docker/redis.conf @@ -0,0 +1,2 @@ +unixsocket /var/run/redis/redis.sock +unixsocketperm 777 \ No newline at end of file diff --git a/legacy/docker/requirements.prod.txt b/legacy/docker/requirements.prod.txt new file mode 100644 index 0000000..90c5a96 --- /dev/null +++ b/legacy/docker/requirements.prod.txt @@ -0,0 +1,9 @@ +wheel +twisted +pyOpenSSL +redis +pyYaML +python-logstash +service_identity +siphashc +Klein diff --git a/legacy/main.py b/legacy/main.py new file mode 100644 index 0000000..0c7e24c --- /dev/null +++ b/legacy/main.py @@ -0,0 +1,129 @@ +import json +import pickle +from os import urandom +from os.path import exists +from string import digits + +from redis import StrictRedis + +# List of errors ZNC can give us +ZNCErrors = ["Error:", "Unable to load", "does not exist", "doesn't exist"] + +configPath = "conf/live/" +templateConfigPath = "conf/templates/" +certPath = "conf/cert/" + +filemap = { + # JSON configs + "config": ["config.json", "configuration", "json"], + "help": ["help.json", "command help", "json"], + "counters": ["counters.json", "counters file", "json"], + "tokens": ["tokens.json", "authentication tokens", "json"], + "aliasdata": ["aliasdata.json", "data for alias generation", "json"], + "alias": ["alias.json", "provisioned alias data", "json"], + "irc": ["irc.json", "IRC network definitions", "json"], + "blacklist": ["blacklist.json", "IRC channel blacklist", "json"], + # Binary (pickle) configs + "network": ["network.dat", "network list", "pickle"], +} + +# Connections to the plain-text interface +connections = {} +# Connections to the JSON interface +relayConnections = {} + +# Mapping of network names to Protocol (IRCClient) instances +IRCPool = {} + +# Mapping of network names to Reactor instances +# Needed for calling .disconnect() +ReactorPool = {} + +# Mapping of network names to Factory instances +# Needed for calling .stopTrying() +FactoryPool = {} + +# Temporary store for channels allocated after a LIST +# Will get purged as the instances fire up and pop() from +# their respective keys in here +TempChan = {} + +# Mapping of command names to their functions +CommandMap = {} + +# Incremented by 1 for each event reaching modules.counters.event() +# and cloned into lastMinuteSample every minute +runningSample = 0 +lastMinuteSample = 0 + +# Generate 16-byte hex key for message checksums +hashKey = urandom(16) +lastEvents = {} + + +# Get networks that are currently online and dedupliate +def liveNets(): + networks = set() + for i in IRCPool.keys(): + networks.add("".join([x for x in i if x not in digits])) + return networks + + +def saveConf(var): + if var in ("help", "aliasdata"): + return # no need to save this + if filemap[var][2] == "json": + with open(configPath + filemap[var][0], "w") as f: + json.dump(globals()[var], f, indent=4) + elif filemap[var][2] == "pickle": + with open(configPath + filemap[var][0], "wb") as f: + pickle.dump(globals()[var], f) + else: + raise Exception("invalid format") + + +def loadConf(var): + if filemap[var][2] == "json": + filename = configPath + filemap[var][0] + # Only take the help from the templates + if var in ("help", "aliasdata"): + filename = templateConfigPath + filemap[var][0] + if not exists(filename): + # Load the template config + if var == "config": + filename = templateConfigPath + filemap[var][0] + else: + # Everything else should be blank + globals()[var] = {} + return + with open(filename, "r") as f: + globals()[var] = json.load(f) + if var == "alias": + # This is a workaround to convert all the keys into integers since JSON + # turns them into strings... + # Dammit Jason! + global alias + alias = {int(x): y for x, y in alias.items()} + + elif filemap[var][2] == "pickle": + try: + with open(configPath + filemap[var][0], "rb") as f: + globals()[var] = pickle.load(f) + except FileNotFoundError: + globals()[var] = {} + else: + raise Exception("invalid format") + + +def initConf(): + for i in filemap.keys(): + loadConf(i) + + +def initMain(): + global r, g + initConf() + r = StrictRedis( + unix_socket_path=config["RedisSocket"], db=config["RedisDBEphemeral"] # noqa + ) # Ephemeral - flushed on quit + g = StrictRedis(unix_socket_path=config["RedisSocket"], db=config["RedisDBPersistent"]) # noqa diff --git a/legacy/modules/__init__.py b/legacy/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/legacy/modules/alias.py b/legacy/modules/alias.py new file mode 100644 index 0000000..85cebc2 --- /dev/null +++ b/legacy/modules/alias.py @@ -0,0 +1,75 @@ +import random +import re + +import main + + +def generate_password(): + return "".join([chr(random.randint(0, 74) + 48) for i in range(32)]) + + +def generate_alias(): + nick = random.choice(main.aliasdata["stubs"]) + rand = random.randint(1, 2) + if rand == 1: + nick = nick.capitalize() + rand = random.randint(1, 4) + while rand == 1: + split = random.randint(0, len(nick) - 1) + nick = nick[:split] + nick[split + 1 :] # noqa: E203 + rand = random.randint(1, 4) + rand = random.randint(1, 3) + if rand == 1 or rand == 4: + nick = random.choice(main.aliasdata["stubs"]) + nick + if rand == 2 or rand == 5: + nick = random.choice(main.aliasdata["stubs"]).capitalize() + nick + if rand > 2: + nick = nick + str(random.randint(0, 100)) + nick = nick[:11] + + altnick = nick + rand = random.randint(1, 3) + if rand == 1: + altnick += "_" + elif rand == 2: + altnick += "1" + else: + altnick = "_" + altnick + + namebase = random.choice(main.aliasdata["realnames"]) + + ident = nick[:10] + rand = random.randint(1, 7) + if rand == 1: + ident = "quassel" + elif rand == 2: + ident = ident.lower() + elif rand == 3: + ident = re.sub("[0-9]", "", nick) + ident = ident[:10] + elif rand == 4: + ident = namebase.split(" ")[0].lower() + ident = ident[:10] + elif rand == 5: + ident = namebase.split(" ")[0] + ident = ident[:10] + elif rand == 6: + ident = re.sub("\s", "", namebase).lower() # noqa: W605 + ident = ident[:10] + + realname = nick + rand = random.randint(1, 5) + if rand == 1: + realname = namebase + elif rand == 2 or rand == 3: + realname = re.sub("[0-9]", "", realname) + if rand == 3 or rand == 4: + realname = realname.capitalize() + + return { + "nick": nick, + "altnick": altnick, + "ident": ident, + "realname": realname, + "emails": [], + } diff --git a/legacy/modules/chankeep.py b/legacy/modules/chankeep.py new file mode 100644 index 0000000..d4f1b39 --- /dev/null +++ b/legacy/modules/chankeep.py @@ -0,0 +1,585 @@ +from copy import deepcopy +from math import ceil + +from twisted.internet.threads import deferToThread + +import main +from modules import helpers +from utils.logging.debug import debug, trace +from utils.logging.log import error, log, warn + + +def getAllChannels(net=None): + """ + Get a list of all channels on all relays. + :return: list of channels + """ + channels = {} + if not net: + nets = main.network.keys() + else: + nets = [net] + for net in nets: + relays = helpers.get_connected_relays(net) + for relay in relays: + if net not in channels: + channels[net] = {} + if relay.num not in channels[net]: + channels[net][relay.num] = [] + for channel in relay.channels: + channels[net][relay.num].append(channel) + # debug(f"getAllChannels(): {channels}") + return channels + + +def getDuplicateChannels(net=None, total=False): + """ + Get a list of duplicate channels. + :return: list of duplicate channels + """ + allChans = getAllChannels(net) + duplicates = {} + for net in allChans.keys(): + net_chans = [] + inst = {} + # add all the channels from this network to a list + for num in allChans[net].keys(): + net_chans.extend(allChans[net][num]) + for channel in net_chans: + count_chan = net_chans.count(channel) + # I don't know why but it works + # this is used in userinfo.delChannels + set_min = 1 + if total: + set_min = 0 + if count_chan > set_min: + inst[channel] = count_chan + if inst: + duplicates[net] = inst + + if total: + return duplicates + + to_part = {} + for net in allChans: + if net in duplicates: + for num in allChans[net].keys(): + for channel in allChans[net][num]: + if channel in duplicates[net].keys(): + if duplicates[net][channel] > 1: + if net not in to_part: + to_part[net] = {} + if num not in to_part[net]: + to_part[net][num] = [] + to_part[net][num].append(channel) + duplicates[net][channel] -= 1 + + return to_part + + +def partChannels(data): + for net in data: + for num in data[net]: + name = f"{net}{num}" + if name in main.IRCPool.keys(): + for channel in data[net][num]: + if channel in main.IRCPool[name].channels: + main.IRCPool[name].part(channel) + log(f"Parted {channel} on {net} - {num}") + + +def getEnabledRelays(net): + """ + Get a list of enabled relays for a network. + :param net: network + :rtype: list of int + :return: list of enabled relay numbers + """ + enabledRelays = [x for x in main.network[net].relays.keys() if main.network[net].relays[x]["enabled"]] + # debug(f"getEnabledRelays() {net}: {enabledRelays}") + return enabledRelays + + +def getConnectedRelays(net): + """ + Get a list of connected relays for a network. + :param net: network + :rtype: list of int + :return: list of relay numbers + """ + enabledRelays = getEnabledRelays(net) + connectedRelays = [] + for i in enabledRelays: + name = net + str(i) + if name in main.IRCPool.keys(): + if main.IRCPool[name].isconnected: + connectedRelays.append(i) + # debug(f"getConnectedRelays() {net}: {connectedRelays}") + return connectedRelays + + +def getActiveRelays(net): + """ + Get a list of active relays for a network. + :param net: network + :rtype: list of int + :return: list of relay numbers + """ + enabledRelays = getEnabledRelays(net) + activeRelays = [] + for i in enabledRelays: + name = net + str(i) + if name in main.IRCPool.keys(): + # debug( + # ( + # f"getActiveRelays() {net}: {i} auth:{main.IRCPool[name].authenticated} " + # f"conn:{main.IRCPool[name].isconnected}" + # ) + # ) + if main.IRCPool[name].authenticated and main.IRCPool[name].isconnected: + activeRelays.append(i) + debug(f"getActiveRelays() {net}: {activeRelays}") + return activeRelays + + +def relayIsActive(net, num): + """ + Check if a relay is active. + :param net: network + :param num: relay number + :rtype: bool + :return: True if relay is active, False otherwise + """ + activeRelays = getActiveRelays(net) + return num in activeRelays + + +def allRelaysActive(net): + """ + Check if all enabled relays are active and authenticated. + :param net: network + :rtype: bool + :return: True if all relays are active and authenticated, False otherwise + """ + activeRelays = getActiveRelays(net) + enabledRelays = getEnabledRelays(net) + relaysActive = len(activeRelays) == len(enabledRelays) + # debug(f"allRelaysActive() {net}: {relaysActive} ({activeRelays}/{enabledRelays})") + return relaysActive + + +def getAverageChanlimit(net): + """ + Get the average channel limit for a network. + :param net: network + :rtype: int + :return: average channel limit + """ + total = 0 + for i in getActiveRelays(net): + name = net + str(i) + if name in main.IRCPool.keys(): + total += main.IRCPool[name].chanlimit + avg_chanlimit = total / len(getActiveRelays(net)) + debug(f"getAverageChanlimit() {net}: {avg_chanlimit}") + return avg_chanlimit + + +def getSumChanlimit(net): + """ + Get the sum of all channel limits for a network. + :param net: network + :rtype: int + :return: sum of channel limits + """ + total = 0 + for i in getActiveRelays(net): + name = net + str(i) + if name in main.IRCPool.keys(): + total += main.IRCPool[name].chanlimit + return total + + +def getChanFree(net): + """ + Get a dictionary with the free channel spaces for + each relay, and a channel limit. + Example return: + ({1: 99}, 100) + :param net: network + :return: ({relay: channel spaces}, channel limit) + """ + chanfree = {} + for i in getActiveRelays(net): + name = net + str(i) + if name not in main.IRCPool.keys(): + continue + if not main.IRCPool[name].isconnected: + continue + chanfree[i] = main.IRCPool[name].chanlimit - len(main.IRCPool[name].channels) + + return chanfree + + +def getTotalChans(net): + """ + Get the total number of channels on all relays for a network. + :param net: network + :rtype: int + :return: total number of channels + """ + total = 0 + for i in getActiveRelays(net): + name = net + str(i) + if name in main.IRCPool.keys(): + total += len(main.IRCPool[name].channels) + return total + + +def emptyChanAllocate(net, flist): + """ + Allocate channels to relays. + :param net: network + :param flist: list of channels to allocate + :param new: list of newly provisioned relays to account for + :rtype: dict + :return: dictionary of {relay: list of channels}""" + + # Get the free channel spaces for each relay + chanfree = getChanFree(net) + if not chanfree: + return + + # Pretend the newly provisioned relays are already on the network + # for i in new: + # chanfree[0][i] = chanfree[1] + allocated = {} + + newlist = list(flist) + chan_slots_used = getTotalChans(net) + max_chans = getSumChanlimit(net) - chan_slots_used + trunc_list = newlist[:max_chans] + debug(f"emptyChanAllocate() {net}: newlist:{len(newlist)} trunc_list:{len(trunc_list)}") + + for i in chanfree.keys(): + for x in range(chanfree[i]): + if not len(trunc_list): + break + if i in allocated.keys(): + allocated[i].append(trunc_list.pop()) + else: + allocated[i] = [trunc_list.pop()] + return allocated + + +def populateChans(net, clist): + """ + Populate channels on relays. + Stores channels to join in a list in main.TempChan[net][num] + :param net: network + :param clist: list of channels to join + :param new: list of newly provisioned relays to account for""" + # divided = array_split(clist, relay) + allocated = emptyChanAllocate(net, clist) + trace(f"populateChans() allocated:{allocated}") + if not allocated: + return + for i in allocated.keys(): + if net in main.TempChan.keys(): + main.TempChan[net][i] = allocated[i] + else: + main.TempChan[net] = {i: allocated[i]} + trace(f"populateChans() TempChan {net}{i}: {allocated[i]}") + + +def notifyJoin(net): + """ + Notify relays to join channels. + They will pull from main.TempChan and remove channels they join. + :param net: network + """ + for i in getActiveRelays(net): + name = net + str(i) + if name in main.IRCPool.keys(): + trace(f"notifyJoin() {name}") + main.IRCPool[name].checkChannels() + + +def minifyChans(net, listinfo, as_list=False): + """ + Remove channels from listinfo that are already covered by a relay. + :param net: network + :param listinfo: list of channels to check + :type listinfo: list of [channel, num_users] + :return: list of channels with joined channels removed + :rtype: list of [channel, num_users] + """ + # We want to make this reusable for joining a bunch of channels. + if as_list: + channel_list = listinfo + + if not allRelaysActive(net): + error("All relays for %s are not active, cannot minify list" % net) + return False + for i in getConnectedRelays(net): + name = net + str(i) + for x in main.IRCPool[name].channels: + if as_list: + for y in channel_list: + if y == x: + channel_list.remove(y) + else: + for y in listinfo: + if y[0] == x: + listinfo.remove(y) + if not as_list: + if not listinfo: + log("We're on all the channels we want to be on, dropping LIST") + return False + if as_list: + return channel_list + else: + return listinfo + + +def keepChannels(net, listinfo, mean, sigrelay, relay): + """ + Minify channels, determine whether we can cover all the channels + on the network, or need to use 'significant' mode. + Truncate the channel list to available channel spaces. + Allocate these channels to relays. + Notify relays that they should pull from TempChan to join. + :param net: network + :param listinfo: list of [channel, num_users] lists + :param mean: mean of channel population + :param sigrelay: number of relays needed to cover significant channels + :param relay: number of relays needed to cover all channels + :param chanlimit: maximum number of channels to allocate to a relay + """ + listinfo = minifyChans(net, listinfo) + if not listinfo: + return + if relay <= main.config["ChanKeep"]["SigSwitch"]: # we can cover all of the channels + coverAll = True + elif relay > main.config["ChanKeep"]["SigSwitch"]: # we cannot cover all of the channels + coverAll = False + # if not sigrelay <= main.config["ChanKeep"]["MaxRelay"]: + # error("Network %s is too big to cover: %i relays required" % (net, sigrelay)) + # return + num_instances = len(getActiveRelays(net)) + debug(f"keepChannels() {net} instances:{num_instances}") + chan_slots_used = getTotalChans(net) + debug(f"keepChannels() slots_used:{chan_slots_used}") + # max_chans = (chanlimit * num_instances) - chan_slots_used + max_chans = getSumChanlimit(net) - chan_slots_used + if max_chans < 0: + max_chans = 0 + debug(f"keepChannels() max_chans:{max_chans}") + if coverAll: + # needed = relay - len(getActiveRelays(net)) + # if needed: + # debug(f"keepChannels() coverAll asking to provision {needed} relays for {net} relay:{relay}") + # newNums = modules.provision.provisionMultipleRelays(net, needed) + # else: + # newNums = [] + listinfo_sort = sorted(listinfo, reverse=True, key=lambda x: x[1]) + if len(listinfo_sort) > max_chans: + max_chans = len(listinfo_sort) - 1 + + flist = [i[0] for i in listinfo_sort] + + flist = flist[:max_chans] + debug(f"keepChannels() {net}: joining {len(flist)}/{len(listinfo_sort)} channels") + trace(f"keepChannels() {net}: joining:{flist}") + populateChans(net, flist) + else: + # needed = sigrelay - len(getActiveRelays(net)) + # if needed: + # debug(f"keepChannels() NOT coverAll asking to provision {needed} relays for {net} sigrelay:{sigrelay}") + # newNums = modules.provision.provisionMultipleRelays(net, needed) + # else: + # newNums = [] + listinfo_sort = sorted(listinfo, reverse=True, key=lambda x: x[1]) + trace(f"keepChannels() {net}: listinfo_sort:{listinfo_sort}") + if len(listinfo_sort) > max_chans: + max_chans = len(listinfo_sort) - 1 + debug(f"keepChannels() {net}: new max_chans:{max_chans}") + + siglist = [i[0] for i in listinfo if int(i[1]) > mean] + trace(f"keepChannels() {net}: new siglist:{siglist}") + + siglist = siglist[:max_chans] + trace(f"keepChannels() {net}: truncated siglist:{siglist}") + + trace(f"keepChannels() {net}: siglist:{siglist} max_chans:{max_chans} len_sig:{len(listinfo_sort)}") + debug(f"keepChannels() {net}: joining {len(siglist)}/{len(listinfo_sort)} channels") + trace(f"keepChannels() {net}: joining:{siglist}") + populateChans(net, siglist) + notifyJoin(net) + + +def joinSingle(net, channel): + """ + Join a channel on a relay. + Use ECA to determine which relay to join on. + :param net: network + :param channel: channel to join + :return: relay number that joined the channel + :rtype: int + """ + if "," in channel: + channels = channel.split(",") + channels = minifyChans(net, channels, as_list=True) + + else: + channels = [channel] + populateChans(net, channels) + notifyJoin(net) + return True + + +def partSingle(net, channel): + """ + Iterate over all the relays of net and part channels matching channel. + :param net: network + :param channel: channel to part + :return: list of relays that parted the channel + :rtype: list of str + """ + parted = [] + for i in getConnectedRelays(net): + name = f"{net}{i}" + if name in main.IRCPool.keys(): + if channel in main.IRCPool[name].channels: + main.IRCPool[name].part(channel) + parted.append(str(i)) + return parted + + +def nukeNetwork(net): + """ + Remove network records. + :param net: network""" + # purgeRecords(net) + # p = main.g.pipeline() + main.g.delete("analytics.list." + net) + # p.delete("list."+net) + # p.execute() + + +# def nukeNetwork(net): +# deferToThread(_nukeNetwork, net) + + +def _initialList(net, num, listinfo): + """ + Called when a relay receives a full LIST response. + Run statistics to determine how many channels are significant. + This is done by adding all the numbers of users on the channels together, + then dividing by the number of channels. + * cumul - cumulative sum of all channel membership + * siglength - number of significant channels + * listlength - number of channels in the list + * sigrelay - number of relays needed to cover siglength + * relay - number of relays needed to cover all channels + :param net: network + :param num: relay number + :param listinfo: list of [channel, num_users] lists + :param chanlimit: maximum number of channels the relay can join + """ + listlength = len(listinfo) + cumul = 0 + try: + cumul += sum(int(i[1]) for i in listinfo) + except TypeError: + warn("Bad LIST data received from %s - %i" % (net, num)) + return + mean = round(cumul / listlength, 2) + siglength = 0 + insiglength = 0 + sigcumul = 0 + insigcumul = 0 + for i in listinfo: + if int(i[1]) > mean: + siglength += 1 + sigcumul += int(i[1]) + elif int(i[1]) < mean: + insiglength += 1 + insigcumul += int(i[1]) + + avg_chanlimit = getAverageChanlimit(net) + sigrelay = ceil(siglength / avg_chanlimit) + relay = ceil(listlength / avg_chanlimit) + + cur_relays = len(getActiveRelays(net)) + sig_relays_missing = sigrelay - cur_relays + all_relays_missing = relay - cur_relays + + abase = "analytics.list.%s" % net + main.g.delete(abase) + p = main.g.pipeline() + + # See docstring for meanings + p.hset(abase, "mean", mean) + p.hset(abase, "total_chans", listlength) + p.hset(abase, "big_chans", siglength) + p.hset(abase, "small_chans", insiglength) + p.hset(abase, "big_chan_perc", round(siglength / listlength * 100, 2)) + p.hset(abase, "small_chan_perc", round(insiglength / listlength * 100, 2)) + p.hset(abase, "total_cumul_mem", cumul) + p.hset(abase, "big_chan_cumul_mem", sigcumul) + p.hset(abase, "small_chan_cumul_mem", insigcumul) + p.hset(abase, "relays_for_all_chans", relay) + p.hset(abase, "relays_for_big_chans", sigrelay) + p.hset(abase, "relays_for_small_chans", ceil(insiglength / avg_chanlimit)) + p.hset(abase, "sig_relays_missing", sig_relays_missing) + p.hset(abase, "all_relays_missing", all_relays_missing) + debug( + ( + f"_initialList() net:{net} num:{num} listlength:{listlength} " + f"mean:{mean} siglength:{siglength} insiglength:{insiglength} " + f"sigrelay:{sigrelay} relay:{relay} avg_chanlimit:{avg_chanlimit}" + ) + ) + + # Purge existing records before writing + # purgeRecords(net) + # for i in listinfo: + # p.rpush(netbase+"."+i[0], i[1]) + # p.rpush(netbase+"."+i[0], i[2]) + # p.sadd(netbase, i[0]) + + p.execute() + debug("List parsing completed on %s" % net) + keepChannels(net, listinfo, mean, sigrelay, relay) + + # return (listinfo, mean, sigrelay, relay) + + +def convert(data): + """ + Recursively convert a dictionary. + """ + if isinstance(data, bytes): + return data.decode("ascii") + if isinstance(data, dict): + return dict(map(convert, data.items())) + if isinstance(data, tuple): + return map(convert, data) + if isinstance(data, list): + return list(map(convert, data)) + return data + + +def getListInfo(net): + abase = f"analytics.list.{net}" + info = main.g.hgetall(abase) + return convert(info) + + +def initialList(net, num, listinfo): + """ + Run _initialList in a thread. + See above docstring. + """ + deferToThread(_initialList, net, num, deepcopy(listinfo)) diff --git a/legacy/modules/counters.py b/legacy/modules/counters.py new file mode 100644 index 0000000..64407e7 --- /dev/null +++ b/legacy/modules/counters.py @@ -0,0 +1,44 @@ +from twisted.internet.task import LoopingCall + +import main + + +def event(name, eventType): + if "local" not in main.counters.keys(): + main.counters["local"] = {} + if "global" not in main.counters.keys(): + main.counters["global"] = {} + if name not in main.counters["local"].keys(): + main.counters["local"][name] = {} + if eventType not in main.counters["local"][name].keys(): + main.counters["local"][name][eventType] = 0 + + if eventType not in main.counters["global"]: + main.counters["global"][eventType] = 0 + + main.counters["local"][name][eventType] += 1 + main.counters["global"][eventType] += 1 + main.runningSample += 1 + + +def getEvents(name=None): + if name is None: + if "global" in main.counters.keys(): + return main.counters["global"] + else: + return None + else: + if name in main.counters["local"].keys(): + return main.counters["local"][name] + else: + return None + + +def takeSample(): + main.lastMinuteSample = main.runningSample + main.runningSample = 0 + + +def setupCounterLoop(): + lc = LoopingCall(takeSample) + lc.start(60) diff --git a/legacy/modules/helpers.py b/legacy/modules/helpers.py new file mode 100644 index 0000000..edd7ac2 --- /dev/null +++ b/legacy/modules/helpers.py @@ -0,0 +1,72 @@ +import main +from modules import chankeep +from utils.logging.debug import debug + + +def get_first_relay(net): + """ + Get the first relay in the network. + :param net: the network + :param num: number or relay + :return: IRCPool instance for the IRC bot + """ + cur_relay = 0 + max_relay = max(main.network[net].relays.keys()) + debug(f"get_first_relay() {net}: max_relay:{max_relay}") + activeRelays = chankeep.getActiveRelays(net) + + debug(f"get_first_relay() {net}: activeRelays:{activeRelays}") + while cur_relay != max_relay: + cur_relay += 1 + if cur_relay not in activeRelays: + continue + name = net + str(cur_relay) + if name in main.IRCPool.keys(): + # debug(f"get_first_relay() {net}: found relay {name}") + return main.IRCPool[name] + return None + + +def is_first_relay(net, num): + """ + Determine if we are the first relay for the network. + :param net: the network + :param num: number or relay + :return: True if we are the first relay, False otherwise + """ + first_relay = get_first_relay(net) + if not first_relay: + return False + return first_relay.num == num + + +def get_active_relays(net): + """ + Get all active instances for the network. + :param net: the network + :return: list of active instances + :rtype: list of IRCPool instances + """ + active_nums = chankeep.getActiveRelays(net) + active_insts = [] + for num in active_nums: + name = net + str(num) + if name in main.IRCPool.keys(): + active_insts.append(main.IRCPool[name]) + return active_insts + + +def get_connected_relays(net): + """ + Get all connected instances for the network. + :param net: the network + :return: list of active instances + :rtype: list of IRCPool instances + """ + active_nums = chankeep.getConnectedRelays(net) + active_insts = [] + for num in active_nums: + name = net + str(num) + if name in main.IRCPool.keys(): + active_insts.append(main.IRCPool[name]) + return active_insts diff --git a/legacy/modules/monitor.py b/legacy/modules/monitor.py new file mode 100644 index 0000000..ae4905c --- /dev/null +++ b/legacy/modules/monitor.py @@ -0,0 +1,80 @@ +import main +from core.logstash import sendLogstashNotification +from core.relay import sendRelayNotification +from modules import userinfo +from utils.dedup import dedup + +order = [ + "type", + "net", + "num", + "channel", + "msg", + "nick", + "ident", + "host", + "mtype", + "user", + "mode", + "modearg", + "realname", + "server", + "status", + "ts", +] + + +def parsemeta(numName, c): + if "channel" not in c.keys(): + c["channel"] = None + # metadata scraping + # need to check if this was received from a relay + # in which case, do not do this + if c["type"] in ["msg", "notice", "action", "topic", "mode"]: + if "muser" in c.keys(): + userinfo.editUser(c["net"], c["muser"]) + # if c["type"] == "mode": + # userinfo.updateMode(c) + elif c["type"] == "nick": + userinfo.renameUser( + c["net"], + c["nick"], + c["muser"], + c["user"], + c["user"] + "!" + c["ident"] + "@" + c["host"], + ) + elif c["type"] == "kick": + userinfo.editUser(c["net"], c["muser"]) + userinfo.delUserByNick(c["net"], c["channel"], c["user"]) + elif c["type"] == "quit": + userinfo.delUserByNetwork(c["net"], c["nick"], c["muser"]) + elif c["type"] == "join": + userinfo.addUser(c["net"], c["channel"], c["nick"], c["muser"]) + elif c["type"] == "part": + userinfo.delUser(c["net"], c["channel"], c["nick"], c["muser"]) + + if "mtype" in c.keys(): + if c["mtype"] == "nick": + userinfo.renameUser( + c["net"], + c["nick"], + c["muser"], + c["user"], + c["user"] + "!" + c["ident"] + "@" + c["host"], + ) + + +def event(numName, c): # yes I'm using a short variable because otherwise it goes off the screen + if dedup(numName, c): + return + + # make a copy of the object with dict() to prevent sending notifications with channel of None + parsemeta(numName, dict(c)) + + if "muser" in c.keys(): + del c["muser"] + sortedKeys = {k: c[k] for k in order if k in c} # Sort dict keys according to order + sortedKeys["src"] = "irc" + if main.config["Logstash"]["Enabled"]: + sendLogstashNotification(sortedKeys) + sendRelayNotification(sortedKeys) diff --git a/legacy/modules/network.py b/legacy/modules/network.py new file mode 100644 index 0000000..b07f831 --- /dev/null +++ b/legacy/modules/network.py @@ -0,0 +1,164 @@ +from copy import deepcopy + +from twisted.internet import reactor +from twisted.internet.ssl import DefaultOpenSSLContextFactory + +import main +from core.bot import IRCBotFactory +from modules import alias +from modules.chankeep import nukeNetwork +from modules.provision import provisionRelay +from modules.regproc import needToRegister +from utils.deliver_relay_commands import deliverRelayCommands +from utils.get import getRelay +from utils.logging.log import log + + +def migrate(): + existing = deepcopy(main.network) + log("Migrating network configuration") + log(f"Existing network configuration: {existing}") + for net, net_inst in existing.items(): + log(f"Migrating network {net}") + net = net_inst.net + host = net_inst.host + port = net_inst.port + security = net_inst.security + auth = net_inst.auth + last = net_inst.last + relays = net_inst.relays + aliases = net_inst.aliases + + new_net = Network(net, host, port, security, auth) + log(f"New network for {net}: {new_net}") + new_net.last = last + new_net.relays = relays + new_net.aliases = aliases + main.network[net] = new_net + main.saveConf("network") + log("Finished migrating network configuration") + + +class Network: + def __init__(self, net, host, port, security, auth): + self.net = net + self.host = host + self.port = port + self.security = security + self.auth = auth + self.chanlimit = None + + self.last = 1 + self.relays = {} + self.aliases = {} + + def add_relay(self, num=None): + # Grrrrrrrrrr + self.last = int(self.last) + if not num: + num = self.last + self.last += 1 + elif num == self.last: + self.last += 1 + registered = False + if not needToRegister(self.net): + registered = True + # Don't need to register if it's been disabled in definitions, + # so we'll pretend we already did + self.relays[num] = { + "enabled": main.config["ConnectOnCreate"], + "net": self.net, + "id": num, + "registered": registered, + } + password = alias.generate_password() + if num not in main.alias.keys(): + main.alias[num] = alias.generate_alias() + main.saveConf("alias") + self.aliases[num] = {"password": password} + # if main.config["ConnectOnCreate"]: -- Done in provision + # self.start_bot(num) + provisionRelay(num, self.net) + return num, main.alias[num]["nick"] + + def enable_relay(self, num): + """ + Enable a relay for this network. + Send a command to ZNC to connect. + """ + self.relays[num]["enabled"] = True + user = main.alias[num]["nick"] + commands = {"status": ["Connect"]} + name = f"{self.net}{num}" + deliverRelayCommands(num, commands, user=user + "/" + self.net) + main.saveConf("network") + if name not in main.IRCPool.keys(): + self.start_bot(num) + + def disable_relay(self, num): + """ + Disable a relay for this network. + Send a command to ZNC to disconnect. + Stop trying to connect to the relay. + """ + self.relays[num]["enabled"] = False + user = main.alias[num]["nick"] + # relay = main.network[spl[1]].relays[relayNum] + commands = {"status": ["Disconnect"]} + name = f"{self.net}{num}" + deliverRelayCommands(num, commands, user=user + "/" + self.net) + main.saveConf("network") + if name in main.ReactorPool.keys(): + if name in main.FactoryPool.keys(): + main.FactoryPool[name].stopTrying() + main.ReactorPool[name].disconnect() + if name in main.IRCPool.keys(): + del main.IRCPool[name] + del main.ReactorPool[name] + del main.FactoryPool[name] + + def killAliases(self, aliasList): + for i in aliasList: + name = self.net + str(i) + if name in main.ReactorPool.keys(): + if name in main.FactoryPool.keys(): + main.FactoryPool[name].stopTrying() + main.ReactorPool[name].disconnect() + if name in main.IRCPool.keys(): + del main.IRCPool[name] + del main.ReactorPool[name] + del main.FactoryPool[name] + + def delete_relay(self, id): + del self.relays[id] + del self.aliases[id] + # del main.alias[id] - Aliases are global per num, so don't delete them! + self.killAliases([id]) + + def seppuku(self): + # Removes all bots in preperation for deletion + self.killAliases(self.relays.keys()) + nukeNetwork(self.net) + + def start_bot(self, num): + # a single name is given to relays in the backend + # e.g. freenode1 for the first relay on freenode network + keyFN = main.certPath + main.config["Key"] + certFN = main.certPath + main.config["Certificate"] + contextFactory = DefaultOpenSSLContextFactory( + keyFN.encode("utf-8", "replace"), certFN.encode("utf-8", "replace") + ) + bot = IRCBotFactory(self.net, num) + # host, port = self.relays[num]["host"], self.relays[num]["port"] + host, port = getRelay(num) + rct = reactor.connectSSL(host, port, bot, contextFactory) + name = self.net + str(num) + main.ReactorPool[name] = rct + main.FactoryPool[name] = bot + + log("Started bot on relay %s on %s" % (num, self.host)) + + def start_bots(self): + for num in self.relays.keys(): + if self.relays[num]["enabled"]: + self.start_bot(num) diff --git a/legacy/modules/provision.py b/legacy/modules/provision.py new file mode 100644 index 0000000..db5ddcd --- /dev/null +++ b/legacy/modules/provision.py @@ -0,0 +1,95 @@ +from twisted.internet import reactor + +import main +import modules.regproc +from utils.deliver_relay_commands import deliverRelayCommands +from utils.logging.log import warn + + +def provisionUserNetworkData(num, nick, altnick, ident, realname, network, host, port, security): + commands = {} + stage2commands = {} + stage2commands["status"] = [] + commands["controlpanel"] = [] + user = nick.lower() + commands["controlpanel"].append("AddUser %s %s" % (user, main.config["Relay"]["Password"])) + commands["controlpanel"].append("AddNetwork %s %s" % (user, network)) + commands["controlpanel"].append("Set Nick %s %s" % (user, nick)) + commands["controlpanel"].append("Set Altnick %s %s" % (user, altnick)) + commands["controlpanel"].append("Set Ident %s %s" % (user, ident)) + commands["controlpanel"].append("Set RealName %s %s" % (user, realname)) + if security == "ssl": + commands["controlpanel"].append("SetNetwork TrustAllCerts %s %s true" % (user, network)) # Don't judge me + commands["controlpanel"].append("AddServer %s %s %s +%s" % (user, network, host, port)) + elif security == "plain": + commands["controlpanel"].append("AddServer %s %s %s %s" % (user, network, host, port)) + if not main.config["ConnectOnCreate"]: + stage2commands["status"].append("Disconnect") + if main.config["Toggles"]["CycleChans"]: + stage2commands["status"].append("LoadMod disconkick") + stage2commands["status"].append("LoadMod chansaver") + inst = modules.regproc.selectInst(network) + if "setmode" in inst.keys(): + stage2commands["status"].append("LoadMod perform") + # stage2commands["perform"].append("add mode %nick% +"+inst["setmode"]) + deliverRelayCommands(num, commands, stage2=[[user + "/" + network, stage2commands]]) + + +def provisionAuthenticationData(num, nick, network, auth, password): + commands = {} + commands["status"] = [] + user = nick.lower() + if auth == "sasl": + commands["sasl"] = [] + commands["status"].append("UnloadMod nickserv") + commands["status"].append("LoadMod sasl") + commands["sasl"].append("Mechanism plain") + commands["sasl"].append("Set %s %s" % (nick, password)) + elif auth == "ns": + commands["nickserv"] = [] + commands["status"].append("UnloadMod sasl") + commands["status"].append("LoadMod nickserv") + commands["nickserv"].append("Set %s" % password) + inst = modules.regproc.selectInst(network) + if "setmode" in inst.keys(): + # perform is loaded above + # commands["status"].append("LoadMod perform") + commands["perform"] = ["add mode %nick% +" + inst["setmode"]] + deliverRelayCommands(num, commands, user=user + "/" + network) + + +def provisionRelay(num, network): # provision user and network data + aliasObj = main.alias[num] + # alias = aliasObj["nick"] + nick = aliasObj["nick"] + altnick = aliasObj["altnick"] + ident = aliasObj["ident"] + realname = aliasObj["realname"] + provisionUserNetworkData( + num, + nick, + altnick, + ident, + realname, + network, + main.network[network].host, + main.network[network].port, + main.network[network].security, + ) + if main.config["ConnectOnCreate"]: + reactor.callLater(10, main.network[network].start_bot, num) + + +def provisionMultipleRelays(net, relaysNeeded): + if not relaysNeeded: + return [] + if not main.config["ChanKeep"]["Provision"]: + warn(f"Asked to create {relaysNeeded} relays for {net}, but provisioning is disabled") + return [] + numsProvisioned = [] + for i in range(relaysNeeded): + num, alias = main.network[net].add_relay() + numsProvisioned.append(num) + provisionRelay(num, net) + main.saveConf("network") + return numsProvisioned diff --git a/legacy/modules/regproc.py b/legacy/modules/regproc.py new file mode 100644 index 0000000..e9e2e1f --- /dev/null +++ b/legacy/modules/regproc.py @@ -0,0 +1,233 @@ +from copy import deepcopy +from random import choice + +import main +from modules import provision +from utils.logging.debug import debug +from utils.logging.log import error + + +def needToRegister(net): + # Check if the network does not support authentication + networkObj = main.network[net] + if networkObj.auth == "none": + return False + # Check if the IRC network definition has registration disabled + inst = selectInst(net) + if "register" in inst.keys(): + if inst["register"]: + return True + else: + return False + + +def needToAuth(net): + networkObj = main.network[net] + if networkObj.auth == "none": + return False + return True + + +def selectInst(net): + if net in main.irc.keys(): + inst = deepcopy(main.irc[net]) + for i in main.irc["_"].keys(): + if i not in inst: + inst[i] = main.irc["_"][i] + else: + inst = deepcopy(main.irc["_"]) + return inst + + +def substitute(net, num, token=None): + inst = selectInst(net) + alias = main.alias[num] + gotemail = False + if "emails" in alias: + # First priority is explicit email lists + if alias["emails"]: + email = choice(alias["emails"]) + gotemail = True + if "domains" in inst: + if inst["domains"]: + if not gotemail: + domain = choice(inst["domains"]) + email = f"{alias['nickname']}@{domain}" + gotemail = True + if not gotemail: + inst["email"] = False + nickname = alias["nick"] + # username = nickname + "/" + net + password = main.network[net].aliases[num]["password"] + # inst["email"] = inst["email"].replace("{nickname}", nickname) + + name = f"{net}{num}" + if name in main.IRCPool: + curnick = main.IRCPool[name].nickname + else: + curnick = nickname + for i in inst.keys(): + if not isinstance(inst[i], str): + continue + inst[i] = inst[i].replace("{nickname}", nickname) + inst[i] = inst[i].replace("{curnick}", curnick) + inst[i] = inst[i].replace("{password}", password) + if gotemail: + inst[i] = inst[i].replace("{email}", email) + if token: + inst[i] = inst[i].replace("{token}", token) + return inst + + +def registerAccount(net, num): + debug("Attempting to register: %s - %i" % (net, num)) + sinst = substitute(net, num) + if not sinst: + error(f"Register account failed for {net} - {num}") + return + if not sinst["email"]: + error(f"Could not get email for {net} - {num}") + return + if not sinst["register"]: + error("Cannot register for %s: function disabled" % (net)) + return False + name = net + str(num) + if not main.IRCPool[name].authenticated: + main.IRCPool[name].msg(sinst["entity"], sinst["registermsg"]) + + +def confirmAccount(net, num, token): + sinst = substitute(net, num, token=token) + name = net + str(num) + if name in main.IRCPool: + main.IRCPool[name].msg(sinst["entity"], sinst["confirm"]) + enableAuthentication(net, num) + + +def confirmRegistration(net, num, negativepass=None): + obj = main.network[net] + name = net + str(num) + if name in main.IRCPool.keys(): + if negativepass is not None: + main.IRCPool[name].regPing(negativepass=negativepass) + return + debug("Relay authenticated: %s - %i" % (net, num)) + main.IRCPool[name].authenticated = True + main.IRCPool[name].recheckList() + if obj.relays[num]["registered"]: + return + if name in main.IRCPool.keys(): + if main.IRCPool[name]._regAttempt: + try: + main.IRCPool[name]._regAttempt.cancel() + except: # noqa + pass + obj.relays[num]["registered"] = True + main.saveConf("network") + + +def attemptManualAuthentication(net, num): + sinst = substitute(net, num) + identifymsg = sinst["identifymsg"] + entity = sinst["entity"] + name = f"{net}{num}" + if name not in main.IRCPool: + return + main.IRCPool[name].sendmsg(entity, identifymsg, in_query=True) + + +def enableAuthentication(net, num, jump=True, run_now=False): + obj = main.network[net] + nick = main.alias[num]["nick"] + auth = obj.auth + name = f"{net}{num}" + if name not in main.IRCPool: + return + # uname = main.alias[num]["nick"] + "/" + net + password = main.network[net].aliases[num]["password"] + provision.provisionAuthenticationData(num, nick, net, auth, password) # Set up for auth + if jump: + main.IRCPool[name].msg(main.config["Tweaks"]["ZNC"]["Prefix"] + "status", "Jump") + if run_now: + attemptManualAuthentication(net, num) + if selectInst(net)["check"] is False: + confirmRegistration(net, num) + + +def get_unregistered_relays(net=None): + """ + Get a dict of unregistereed relays, either globally or + for a network. + Returns: + {"net": [["nick1", 1], ["nick2", 2], ...]} + """ + unreg = {} + if net: + nets = [net] + else: + nets = main.network.keys() + for i in nets: + for num in main.network[i].relays.keys(): + if not main.network[i].relays[num]["registered"]: + nick = main.alias[num]["nick"] + if i in unreg: + unreg[i].append([nick, num]) + else: + unreg[i] = [[nick, num]] + return unreg + + +def registerTest(c): + sinst = substitute(c["net"], c["num"]) + name = c["net"] + str(c["num"]) + net = c["net"] + num = c["num"] + if sinst["check"] is False: + return + if "msg" in c.keys() and not c["msg"] is None: + if sinst["negative"]: + if name in main.IRCPool.keys(): + if main.IRCPool[name]._negativePass is not True: + if c["type"] == "query" and c["nick"] == sinst["entity"]: + if sinst["checknegativemsg"] in c["msg"]: + confirmRegistration( + c["net"], c["num"], negativepass=False + ) # Not passed negative check, report back + return + if sinst["checkendnegative"] in c["msg"]: + confirmRegistration( + c["net"], c["num"], negativepass=True + ) # Passed the negative check, report back + return + if sinst["ping"]: + if sinst["checkmsg2"] in c["msg"] and c["nick"] == sinst["entity"]: + confirmRegistration(c["net"], c["num"]) + debug( + ( + f"registerTest() {net} - {num} passed ping:checkmsg2 " + f"check, {sinst['checkmsg2']} present in message" + ) + ) + return + if sinst["checktype"] == "msg": + if sinst["checkmsg"] in c["msg"]: + confirmRegistration(c["net"], c["num"]) + debug( + ( + f"registerTest() {net} - {num} passed checktype:msg:checkmsg check, " + f"{sinst['checkmsg']} present in message" + ) + ) + return + elif sinst["checktype"] == "mode": + if c["type"] == "self": + if c["mtype"] == "mode": + if sinst["checkmode"] in c["mode"] and c["status"] is True: + confirmRegistration(c["net"], c["num"]) + debug( + ( + f"registerTest() {net} - {num} passed checktype:mode:checkmost check, " + f"{sinst['checkmode']} present in mode" + ) + ) + return diff --git a/legacy/modules/userinfo.py b/legacy/modules/userinfo.py new file mode 100644 index 0000000..5180394 --- /dev/null +++ b/legacy/modules/userinfo.py @@ -0,0 +1,304 @@ +from twisted.internet.threads import deferToThread + +import main +from modules import chankeep +from utils.logging.debug import debug, trace +from utils.logging.log import warn +from utils.parsing import parsen + + +def getWhoSingle(name, query): + result = main.r.sscan("live.who." + name, 0, query, count=999999) + if result[1] == []: + return None + return (i.decode() for i in result[1]) + + +def getWho(query): + result = {} + for i in main.network.keys(): + f = getWhoSingle(i, query) + if f: + result[i] = f + return result + + +def getChansSingle(name, nick): + nick = ("live.chan." + name + "." + i for i in nick) + result = main.r.sinter(*nick) + if len(result) == 0: + return None + return (i.decode() for i in result) + + +def getChanList(name, nick): + chanspace = "live.chan." + name + "." + nick + result = main.r.smembers(chanspace) + if len(result) == 0: + return None + return (i.decode() for i in result) + + +def getTotalChanNum(net): + """ + Get the number of channels a network has. + """ + chans = main.r.keys(f"live.who.{net}.*") + return len(chans) + + +def getUserNum(name, channels): + """ + Get the number of users on a list of channels. + """ + chanspace = ("live.who." + name + "." + i for i in channels) + results = {} + for channel, space in zip(channels, chanspace): + results[channel] = main.r.scard(space) + return results + + +def getChanNum(name, nicks): + """ + Get the number of channels a list of users are on. + """ + nickspace = ("live.chan." + name + "." + i for i in nicks) + results = {} + for nick, space in zip(nicks, nickspace): + results[nick] = main.r.scard(space) + return results + + +def getChans(nick): + result = {} + for i in main.network.keys(): + f = getChansSingle(i, nick) + if f: + result[i] = f + return result + + +def getUsersSingle(name, nick): + nick = ("live.who." + name + "." + i for i in nick) + result = main.r.sinter(*nick) + if len(result) == 0: + return None + return (i.decode() for i in result) + + +def getUsers(nick): + result = {} + for i in main.network.keys(): + f = getUsersSingle(i, nick) + if f: + result[i] = f + return result + + +def getNumWhoEntries(name): + return main.r.scard("live.who." + name) + + +def getNumTotalWhoEntries(): + total = 0 + for i in main.network.keys(): + total += getNumWhoEntries(i) + return total + + +def getNamespace(name, channel, nick): + gnamespace = "live.who.%s" % name + namespace = "live.who.%s.%s" % (name, channel) + chanspace = "live.chan.%s.%s" % (name, nick) + mapspace = "live.map.%s" % name + return (gnamespace, namespace, chanspace, mapspace) + + +def _initialUsers(name, channel, users): + gnamespace = "live.who.%s" % name + mapspace = "live.map.%s" % name + p = main.r.pipeline() + for i in users: + user = i[0] + "!" + i[1] + "@" + i[2] + p.hset(mapspace, i[0], user) + p.sadd(gnamespace, user) + p.execute() + + +def initialUsers(name, channel, users): + trace("Initialising WHO records for %s on %s" % (channel, name)) + deferToThread(_initialUsers, name, channel, users) + # d.addCallback(testCallback) + + +def _initialNames(name, channel, names): + namespace = "live.who.%s.%s" % (name, channel) + p = main.r.pipeline() + for mode, nick in names: + p.sadd(namespace, nick) + p.sadd("live.chan." + name + "." + nick, channel) + if mode: + p.hset("live.prefix." + name + "." + channel, nick, mode) + p.execute() + + +def initialNames(name, channel, names): + trace("Initialising NAMES records for %s on %s" % (channel, name)) + deferToThread(_initialNames, name, channel, names) + # d.addCallback(testCallback) + + +def editUser(name, user): + gnamespace = "live.who.%s" % name + mapspace = "live.map.%s" % name + parsed = parsen(user) + p = main.r.pipeline() + p.sadd(gnamespace, user) + p.hset(mapspace, parsed[0], user) # add nick -> user mapping + p.execute() + + +def addUser(name, channel, nick, user): + gnamespace, namespace, chanspace, mapspace = getNamespace(name, channel, nick) + p = main.r.pipeline() + p.sadd(gnamespace, user) + p.sadd(namespace, nick) + p.sadd(chanspace, channel) + p.hset(mapspace, nick, user) + p.execute() + + +def delUser(name, channel, nick, user): + gnamespace, namespace, chanspace, mapspace = getNamespace(name, channel, nick) + p = main.r.pipeline() + channels = main.r.smembers(chanspace) + p.srem(namespace, nick) + if channels == {channel.encode()}: # can we only see them on this channel? + p.delete(chanspace) # remove channel tracking entry + p.hdel("live.prefix." + name + "." + channel, nick) # remove prefix tracking entry + p.hdel(mapspace, nick) # remove nick mapping entry + if user: + p.srem(gnamespace, user) # remove global userinfo entry + else: + warn("Attempt to delete nonexistent user: %s" % user) + else: + p.srem(chanspace, channel) # keep up - remove the channel from their list + p.execute() + + +def escape(text): + chars = ["[", "]", "^", "-", "*", "?"] + text = text.replace("\\", "\\\\") + for i in chars: + text = text.replace(i, "\\" + i) + return text + + +def getUserByNick(name, nick): + gnamespace = "live.who.%s" % name # "nick": "nick!ident@host" + mapspace = "live.map.%s" % name + if main.r.hexists(mapspace, nick): + return main.r.hget(mapspace, nick) + else: + warn("Entry doesn't exist: %s on %s - attempting auxiliary lookup" % (nick, mapspace)) + # return False + # legacy code below - remove when map is reliable + usermatch = main.r.sscan(gnamespace, match=escape(nick) + "!*", count=999999999) + if usermatch[1] == []: + warn("No matches found for user query: %s on %s" % (nick, name)) + return False + else: + if len(usermatch[1]) == 1: + user = usermatch[1][0] + return user + else: + warn("Auxiliary lookup failed: %s on %s" % (nick, gnamespace)) + return False + + +def renameUser(name, oldnick, olduser, newnick, newuser): + gnamespace = "live.who.%s" % name + chanspace = "live.chan.%s.%s" % (name, oldnick) + mapspace = "live.map.%s" % name + newchanspace = "live.chan.%s.%s" % (name, newnick) + p = main.r.pipeline() + p.srem(gnamespace, olduser) + p.sadd(gnamespace, newuser) + for i in main.r.smembers(chanspace): + i = i.decode() + p.srem("live.who." + name + "." + i, oldnick) + p.sadd("live.who." + name + "." + i, newnick) + p.hdel(mapspace, oldnick) + p.hset(mapspace, newnick, newuser) + if main.r.exists("live.prefix." + name + "." + i): # if there's a prefix entry for the channel + if main.r.hexists("live.prefix." + name + "." + i, oldnick): # if the old nick is in it + mode = main.r.hget("live.prefix." + name + "." + i, oldnick) # retrieve old modes + p.hset("live.prefix." + name + "." + i, newnick, mode) # set old modes to new nickname + if main.r.exists(chanspace): + p.rename(chanspace, newchanspace) + else: + warn("Key doesn't exist: %s" % chanspace) + p.execute() + + +def delUserByNick(name, channel, nick): # kick + user = getUserByNick(name, nick) + if not user: + return + delUser(name, channel, nick, user) + + +def delUserByNetwork(name, nick, user): # quit + gnamespace = "live.who.%s" % name + chanspace = "live.chan.%s.%s" % (name, nick) + mapspace = "live.chan.%s" % name + p = main.r.pipeline() + p.srem(gnamespace, user) + for i in main.r.smembers(chanspace): + p.srem("live.who." + name + "." + i.decode(), nick) + p.hdel("live.prefix." + name + "." + i.decode(), nick) + + p.delete(chanspace) + p.hdel(mapspace, nick) + p.execute() + + +def _delChannels(net, channels): + gnamespace = "live.who.%s" % net + mapspace = "live.map.%s" % net + p = main.r.pipeline() + for channel in channels: + namespace = "live.who.%s.%s" % (net, channel) + for i in main.r.smembers(namespace): + nick = i.decode() + # user = getUserByNick(net, nick) -- far too many function calls + user = main.r.hget(mapspace, nick) + if not user: + warn("User lookup failed: %s on %s" % (nick, net)) + if main.r.smembers("live.chan." + net + "." + nick) == {channel.encode()}: + if user: + p.srem(gnamespace, user) + + p.delete("live.chan." + net + "." + nick) + p.hdel(mapspace, nick) # remove map entry + else: + p.srem("live.chan." + net + "." + nick, channel) + p.delete(namespace) + p.delete("live.prefix." + net + "." + channel) + p.execute() + + +def delChannels(net, channels): # we have left a channel + trace("Purging channel %s for %s" % (", ".join(channels), net)) + dupes = chankeep.getDuplicateChannels(net, total=True) + print("dupes: %s" % dupes) + if not dupes: + deferToThread(_delChannels, net, channels) + else: + for channel in channels: + if channel in dupes[net]: + if dupes[net][channel] != 0: + channels.remove(channel) + debug(f"Not removing channel {channel} as {net} has {dupes[net][channel]} other relays covering it") + deferToThread(_delChannels, net, channels) + # d.addCallback(testCallback) diff --git a/legacy/requirements.txt b/legacy/requirements.txt new file mode 100644 index 0000000..7dbea1c --- /dev/null +++ b/legacy/requirements.txt @@ -0,0 +1,10 @@ +wheel +pre-commit +twisted +pyOpenSSL +redis +pyYaML +python-logstash +service_identity +siphashc +Klein diff --git a/legacy/runtest.sh b/legacy/runtest.sh new file mode 100755 index 0000000..1ee7505 --- /dev/null +++ b/legacy/runtest.sh @@ -0,0 +1,3 @@ +#!/bin/sh +#pre-commit run -a +python -m unittest discover -s tests -p 'test_*.py' diff --git a/legacy/stack.env b/legacy/stack.env new file mode 100644 index 0000000..b04bec9 --- /dev/null +++ b/legacy/stack.env @@ -0,0 +1,17 @@ +THRESHOLD_LISTENER_HOST=0.0.0.0 +THRESHOLD_LISTENER_PORT=13867 +THRESHOLD_LISTENER_SSL=1 + +THRESHOLD_RELAY_ENABLED=0 +THRESHOLD_RELAY_HOST=0.0.0.0 +THRESHOLD_RELAY_PORT=13868 +THRESHOLD_RELAY_SSL=1 + +THRESHOLD_API_ENABLED=1 +THRESHOLD_API_HOST=0.0.0.0 +THRESHOLD_API_PORT=13869 +PORTAINER_GIT_DIR=.. + +THRESHOLD_CONFIG_DIR=../conf/live/ +THRESHOLD_TEMPLATE_DIR=../conf/example/ +THRESHOLD_CERT_DIR=../conf/cert/ \ No newline at end of file diff --git a/legacy/tests/test_chankeep.py b/legacy/tests/test_chankeep.py new file mode 100644 index 0000000..491a8ce --- /dev/null +++ b/legacy/tests/test_chankeep.py @@ -0,0 +1,117 @@ +from math import ceil +from random import randint +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from modules import chankeep + + +class TestChanKeep(TestCase): + def setUp(self): + self.net = "testnet" + self.num = 1 + self.chanlimit = 100 + chankeep.main.initConf() + chankeep.main.r = MagicMock() + chankeep.main.g = MagicMock() + chankeep.main.g.pipeline = MagicMock() + chankeep.main.config["ChanKeep"]["Provision"] = False + chankeep.getAverageChanlimit = MagicMock() + chankeep.getAverageChanlimit.return_value = self.chanlimit + + self.listinfo = self.generate_listinfo() + self.chan_name_list = [x[0] for x in self.listinfo] + self.chan_member_list = [x[1] for x in self.listinfo] + + def generate_listinfo(self, ranges=None): + """ + Create a fake listinfo. + Where #channel has 192 users, and #channel2 has 188 users. + listinfo = [["#channel", 192], ["#channel2", 188]] + """ + if not ranges: + ranges = [[100, 5, 10], [400, 100, 200], [2, 500, 1000]] + listinfo = [] + for channum, min, max in ranges: + for i in range(channum): + chan_name = f"#num-{channum}-{i}" + chan_users = randint(min, max) + listinfo.append([chan_name, chan_users]) + return listinfo + + def percent_diff(self, a, b): + return (abs(b - a) / a) * 100.0 + + def test_alt_listinfo(self): + # We're looking for a perc of 1000-1100 + # And a sigrelay of 2 + # We only want those 10 big channels + instances = 1 + chanlimit = 5 + max_chans = instances * chanlimit + listinfo = self.generate_listinfo(ranges=[[1000, 1, 2], [200, 400, 800], [10, 1000, 2000]]) + # listinfo_num = [x[1] for x in listinfo] + + listlength = len(listinfo) + cumul = 0 + try: + cumul += sum(int(i[1]) for i in listinfo) + except TypeError: + return + mean = round(cumul / listlength, 2) + siglength = 0 + insiglength = 0 + sigcumul = 0 + insigcumul = 0 + for i in listinfo: + if int(i[1]) > mean: + siglength += 1 + sigcumul += int(i[1]) + elif int(i[1]) < mean: + insiglength += 1 + insigcumul += int(i[1]) + + sigrelay = ceil(siglength / chanlimit) + relay = ceil(listlength / chanlimit) + print( + ( + f"len:{listlength} cumul:{cumul} mean:{mean} " + f"siglength:{siglength} insiglength:{insiglength} " + f"sigrelay:{sigrelay} relay:{relay} sigcumul:{sigcumul} " + f"insigcumul:{insigcumul}" + ) + ) + # We want a return between 1000 and 1100 + + # list_insig = [x for x in listinfo_num if x < mean] + list_sig = [x for x in listinfo if x[1] > mean] + chosen = sorted(list_sig, reverse=True, key=lambda x: x[1])[:max_chans] + self.assertEqual(len(chosen), 5) + + @patch("modules.chankeep.keepChannels") + def test__initialList(self, keepchannels): + chankeep._initialList(self.net, self.num, self.listinfo) + net, passed_list, mean, sigrelay, relay = keepchannels.call_args_list[0][0] + self.assertEqual(net, self.net) + self.assertEqual(passed_list, self.listinfo) + # self.assertEqual(chanlimit, self.chanlimit) + # print(net, mean, sigrelay, relay) + + @patch("modules.chankeep.getChanFree") + def test_empty_chan_allocate(self, getchanfree): + getchanfree.return_value = ({1: 600}, 600) # pretend we have 600 channels free + eca = chankeep.emptyChanAllocate(self.net, self.chan_name_list) + self.assertEqual(len(eca), 1) + num = list(eca.keys())[0] + chans = eca[list(eca.keys())[0]] + self.assertEqual(num, self.num) + self.assertCountEqual(chans, self.chan_name_list) + + getchanfree.return_value = ({1: 100}, 10) + eca = chankeep.emptyChanAllocate(self.net, self.chan_name_list) + self.assertEqual(len(eca), 1) + num = list(eca.keys())[0] + chans = eca[list(eca.keys())[0]] + self.assertEqual(num, self.num) + self.assertEqual(len(chans), 100) + # self.assertCountEqual(chans, self.chan_name_list) diff --git a/legacy/threshold b/legacy/threshold new file mode 100755 index 0000000..3b4bbbf --- /dev/null +++ b/legacy/threshold @@ -0,0 +1,120 @@ +#!/usr/bin/env python +import sys +from codecs import getwriter +from os import getenv +from signal import SIGINT, signal +from sys import stderr, stdout + +from twisted.internet import reactor + +# Webapp stuff +from twisted.internet.protocol import Factory +from twisted.internet.ssl import DefaultOpenSSLContextFactory + +import core.logstash +import main +import modules.counters +from api.views import API +from core.relay import RelayFactory +from core.server import ServerFactory +from utils.cleanup import handler +from utils.loaders.command_loader import loadCommands +from utils.logging.log import log + +Factory.noisy = False + +main.initMain() + +if "--debug" in sys.argv: # yes really + main.config["Debug"] = True +if "--trace" in sys.argv: + main.config["Trace"] = True +if "--migrate" in sys.argv: + from modules.network import migrate + + migrate() + exit() +loadCommands() + + +core.logstash.init_logstash() +signal(SIGINT, handler) # Handle Ctrl-C and run the cleanup routine +stdout = getwriter("utf8")(stdout) # this is a generic fix but we all know +stderr = getwriter("utf8")(stderr) # it's just for the retards on Rizon using +# unicode quit messages for no reason + +trues = ("true", "1", "t", True) + +# Main listener +listener_address = getenv("THRESHOLD_LISTENER_HOST", main.config["Listener"]["Address"]) +listener_port = int(getenv("THRESHOLD_LISTENER_PORT", main.config["Listener"]["Port"])) +listener_ssl = getenv("THRESHOLD_LISTENER_SSL", main.config["Listener"]["UseSSL"]) in trues + +# RelayAPI +relay_enabled = getenv("THRESHOLD_RELAY_ENABLED", main.config["RelayAPI"]["Enabled"]) in trues +relay_address = getenv("THRESHOLD_RELAY_HOST", main.config["RelayAPI"]["Address"]) +relay_port = int(getenv("THRESHOLD_RELAY_PORT", main.config["RelayAPI"]["Port"])) +relay_ssl = getenv("THRESHOLD_RELAY_SSL", main.config["RelayAPI"]["UseSSL"]) in trues + +# Web API +api_enabled = getenv("THRESHOLD_API_ENABLED", main.config["API"]["Enabled"]) in trues +api_address = getenv("THRESHOLD_API_HOST", main.config["API"]["Address"]) +api_port = int(getenv("THRESHOLD_API_PORT", main.config["API"]["Port"])) + +# Debugging +debug_enabled = getenv("THRESHOLD_DEBUG", main.config["Debug"]) in trues +trace_enabled = getenv("THRESHOLD_TRACE", main.config["Trace"]) in trues +if debug_enabled: + main.config["Debug"] = True +if trace_enabled: + main.config["Trace"] = True + + +if __name__ == "__main__": + listener = ServerFactory() + if listener_ssl is True: + reactor.listenSSL( + listener_port, + listener, + DefaultOpenSSLContextFactory( + main.certPath + main.config["Key"], + main.certPath + main.config["Certificate"], + ), + interface=listener_address, + ) + log("Threshold running with SSL on %s:%s" % (listener_address, listener_port)) + else: + reactor.listenTCP( + listener_port, + listener, + interface=listener_address, + ) + log("Threshold running on %s:%s" % (listener_address, listener_port)) + if relay_enabled: + relay = RelayFactory() + if relay_ssl is True: + reactor.listenSSL( + relay_port, + relay, + DefaultOpenSSLContextFactory( + main.certPath + main.config["Key"], + main.certPath + main.config["Certificate"], + ), + interface=relay_address, + ) + log("Threshold relay running with SSL on %s:%s" % (relay_address, relay_port)) + else: + reactor.listenTCP( + relay_port, + relay, + interface=relay_address, + ) + log("Threshold relay running on %s:%s" % (relay_address, relay_port)) + for net in main.network.keys(): + main.network[net].start_bots() + modules.counters.setupCounterLoop() + if api_enabled: + api = API() + api.app.run(api_address, api_port) + else: + reactor.run() diff --git a/legacy/utils/cleanup.py b/legacy/utils/cleanup.py new file mode 100644 index 0000000..057b1bc --- /dev/null +++ b/legacy/utils/cleanup.py @@ -0,0 +1,17 @@ +from twisted.internet import reactor + +import main +from utils.logging.debug import debug +from utils.logging.log import log + + +def handler(sig, frame): + log("Received SIGINT, cleaning up") + cleanup() + + +def cleanup(): + debug("Flushing Redis database") + main.r.flushdb() + reactor.stop() + # sys.exit(1) diff --git a/legacy/utils/dedup.py b/legacy/utils/dedup.py new file mode 100644 index 0000000..b4c44e3 --- /dev/null +++ b/legacy/utils/dedup.py @@ -0,0 +1,27 @@ +from copy import deepcopy +from datetime import datetime +from json import dumps + +from siphashc import siphash + +import main +from utils.logging.debug import debug + + +def dedup(numName, b): + c = deepcopy(b) + if "ts" in c.keys(): + del c["ts"] + c["approxtime"] = str(datetime.utcnow().timestamp())[: main.config["Tweaks"]["DedupPrecision"]] + castHash = siphash(main.hashKey, dumps(c, sort_keys=True)) + del c["approxtime"] + isDuplicate = any(castHash in main.lastEvents[x] for x in main.lastEvents.keys() if not x == numName) + if isDuplicate: + debug("Duplicate: %s" % (c)) + return True + if numName in main.lastEvents.keys(): + main.lastEvents[numName].insert(0, castHash) + main.lastEvents[numName] = main.lastEvents[numName][0 : main.config["Tweaks"]["MaxHash"]] # noqa + else: + main.lastEvents[numName] = [castHash] + return False diff --git a/legacy/utils/deliver_relay_commands.py b/legacy/utils/deliver_relay_commands.py new file mode 100644 index 0000000..84f30f0 --- /dev/null +++ b/legacy/utils/deliver_relay_commands.py @@ -0,0 +1,163 @@ +from datetime import datetime + +from twisted.internet import reactor +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet.ssl import DefaultOpenSSLContextFactory +from twisted.words.protocols.irc import IRCClient + +import main +from core.relay import sendRelayNotification +from modules import userinfo +from utils.get import getRelay +from utils.logging.log import error, log +from utils.logging.send import sendAll +from utils.parsing import parsen + + +# TODO: strip out non-relay functionality +class IRCRelay(IRCClient): + def __init__(self, num, relayCommands, user, stage2): + self.isconnected = False + self.buffer = "" + if user is None: + self.user = main.config["Relay"]["User"] + else: + self.user = user.lower() + password = main.config["Relay"]["Password"] + self.nickname = "relay" + self.realname = "relay" + self.username = self.user + self.password = self.user + ":" + password + + self.relayCommands = relayCommands + self.num = num + self.stage2 = stage2 + self.loop = None + + def privmsg(self, user, channel, msg): + nick, ident, host = parsen(user) + for i in main.ZNCErrors: + if i in msg: + error("ZNC issue:", msg) + if nick[0] == main.config["Tweaks"]["ZNC"]["Prefix"]: + nick = nick[1:] + if nick in self.relayCommands.keys(): + sendAll("[%s] %s -> %s" % (self.num, nick, msg)) + + def irc_ERR_PASSWDMISMATCH(self, prefix, params): + log("%s: relay password mismatch" % self.num) + sendAll("%s: relay password mismatch" % self.num) + + def sendStage2(self): + # [["user", {"sasl": ["message1", "message2"]}], []] + if not len(self.stage2) == 0: + user = self.stage2[0].pop(0) + commands = self.stage2[0].pop(0) + del self.stage2[0] + deliverRelayCommands(self.num, commands, user, self.stage2) + + def signedOn(self): + if not self.isconnected: + self.isconnected = True + # log("signed on as a relay: %s" % self.num) + sleeptime = 0 + increment = 0.8 + for i in self.relayCommands.keys(): + for x in self.relayCommands[i]: + reactor.callLater( + sleeptime, + self.msg, + main.config["Tweaks"]["ZNC"]["Prefix"] + i, + x, + ) + sleeptime += increment + increment += 0.8 + if self.stage2 is not None: + reactor.callLater(sleeptime, self.sendStage2) + reactor.callLater(sleeptime + 5, self.transport.loseConnection) + return + + +class IRCRelayFactory(ReconnectingClientFactory): + def __init__(self, net, num=None, relayCommands=None, user=None, stage2=None): + if net is None: + self.num = num + self.net = None + self.name = "relay - %i" % num + self.relay = True + else: + self.name = net + str(num) + self.num = num + self.net = net + self.relay = False + self.client = None + self.maxDelay = main.config["Tweaks"]["Delays"]["MaxDelay"] + self.initialDelay = main.config["Tweaks"]["Delays"]["InitialDelay"] + self.factor = main.config["Tweaks"]["Delays"]["Factor"] + self.jitter = main.config["Tweaks"]["Delays"]["Jitter"] + + self.relayCommands, self.user, self.stage2 = relayCommands, user, stage2 + + def buildProtocol(self, addr): + + entry = IRCRelay(self.num, self.relayCommands, self.user, self.stage2) + + self.client = entry + return entry + + def clientConnectionLost(self, connector, reason): + if not self.relay: + userinfo.delChannels(self.net, self.client.channels) + if self.client is not None: + self.client.isconnected = False + self.client.authenticated = False + self.client.channels = [] + error = reason.getErrorMessage() + if not self.relay: + log("%s - %i: connection lost: %s" % (self.net, self.num, error)) + sendAll("%s - %i: connection lost: %s" % (self.net, self.num, error)) + ctime = str(datetime.now().isoformat()) + sendRelayNotification( + { + "type": "conn", + "net": self.net, + "num": self.num, + "status": "lost", + "message": error, + "ts": ctime, + } + ) + self.retry(connector) + # ReconnectingClientFactory.clientConnectionLost(self, connector, reason) + + def clientConnectionFailed(self, connector, reason): + if self.client is not None: + self.client.isconnected = False + self.client.authenticated = False + self.client.channels = [] + error = reason.getErrorMessage() + log("%s - %i: connection failed: %s" % (self.net, self.num, error)) + if not self.relay: + sendAll("%s - %s: connection failed: %s" % (self.net, self.num, error)) + ctime = str(datetime.now().isoformat()) + sendRelayNotification( + { + "type": "conn", + "net": self.net, + "num": self.num, + "status": "failed", + "message": error, + "ts": ctime, + } + ) + self.retry(connector) + # ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) + + +def deliverRelayCommands(num, relayCommands, user=None, stage2=None): + keyFN = main.certPath + main.config["Key"] + certFN = main.certPath + main.config["Certificate"] + contextFactory = DefaultOpenSSLContextFactory(keyFN.encode("utf-8", "replace"), certFN.encode("utf-8", "replace")) + bot = IRCRelayFactory(net=None, num=num, relayCommands=relayCommands, user=user, stage2=stage2) + host, port = getRelay(num) + reactor.connectSSL(host, port, bot, contextFactory) diff --git a/legacy/utils/get.py b/legacy/utils/get.py new file mode 100644 index 0000000..d5c2ec5 --- /dev/null +++ b/legacy/utils/get.py @@ -0,0 +1,13 @@ +import main + + +def getRelay(num): + host = main.config["Relay"]["Host"].replace("x", str(num)) + port = int(str(main.config["Relay"]["Port"]).replace("x", str(num).zfill(2))) + # user = main.config["Relay"]["User"] + # password = main.config["Relay"]["Password"] + try: + port = int(port) + except ValueError: + return False + return (host, port) diff --git a/legacy/utils/loaders/command_loader.py b/legacy/utils/loaders/command_loader.py new file mode 100644 index 0000000..2f0756a --- /dev/null +++ b/legacy/utils/loaders/command_loader.py @@ -0,0 +1,25 @@ +from os import listdir + +from main import CommandMap +from utils.logging.debug import debug +from utils.logging.log import error + + +def loadCommands(allowDup=False): + for filename in listdir("commands"): + if filename.endswith(".py") and filename != "__init__.py": + commandName = filename[0:-3] + className = commandName.capitalize() + "Command" + # try: + module = __import__("commands.%s" % commandName) + if commandName not in CommandMap: + CommandMap[commandName] = getattr(getattr(module, commandName), className) + debug("Registered command: %s" % commandName) + else: + if allowDup: + CommandMap[commandName] = getattr(getattr(module, commandName), className) + debug("Registered command: %s" % commandName) + + error("Duplicate command: %s" % (commandName)) + # except Exception as err: + # error("Exception while loading command %s:\n%s" % (commandName, err)) diff --git a/legacy/utils/loaders/single_loader.py b/legacy/utils/loaders/single_loader.py new file mode 100644 index 0000000..65ce147 --- /dev/null +++ b/legacy/utils/loaders/single_loader.py @@ -0,0 +1,25 @@ +import sys +from importlib import reload +from os import listdir + +from main import CommandMap +from utils.logging.debug import debug + + +def loadSingle(commandName): + if commandName + ".py" in listdir("commands"): + className = commandName.capitalize() + "Command" + try: + if commandName in CommandMap.keys(): + reload(sys.modules["commands." + commandName]) + CommandMap[commandName] = getattr(sys.modules["commands." + commandName], className) + debug("Reloaded command: %s" % commandName) + return "RELOAD" + module = __import__("commands.%s" % commandName) + CommandMap[commandName] = getattr(getattr(module, commandName), className) + debug("Registered command: %s" % commandName) + return True + + except Exception as err: + return err + return False diff --git a/legacy/utils/logging/debug.py b/legacy/utils/logging/debug.py new file mode 100644 index 0000000..0c73174 --- /dev/null +++ b/legacy/utils/logging/debug.py @@ -0,0 +1,13 @@ +import main + + +# we need a seperate module to log.py, as log.py is imported by main.py, and we need to access main +# to read the setting +def debug(*data): + if main.config["Debug"]: + print("[DEBUG]", *data) + + +def trace(*data): + if main.config["Trace"]: + print("[TRACE]", *data) diff --git a/legacy/utils/logging/log.py b/legacy/utils/logging/log.py new file mode 100644 index 0000000..1fd2a0a --- /dev/null +++ b/legacy/utils/logging/log.py @@ -0,0 +1,10 @@ +def log(*data): + print("[LOG]", *data) + + +def warn(*data): + print("[WARNING]", *data) + + +def error(*data): + print("[ERROR]", *data) diff --git a/legacy/utils/logging/send.py b/legacy/utils/logging/send.py new file mode 100644 index 0000000..e1a62af --- /dev/null +++ b/legacy/utils/logging/send.py @@ -0,0 +1,40 @@ +import main + + +def sendData(addr, data): + main.connections[addr].send(data) + + +def sendWithPrefix(addr, data, prefix): + toSend = "" + for i in data.split("\n"): + toSend += prefix + " " + i + "\n" + sendData(addr, toSend) + + +def sendSuccess(addr, data): + sendWithPrefix(addr, data, "[y]") + + +def sendFailure(addr, data): + sendWithPrefix(addr, data, "[n]") + + +def sendInfo(addr, data): + sendWithPrefix(addr, data, "[i]") + + +def sendAll(data): + for i in main.connections: + if main.connections[i].authed: + main.connections[i].send(data) + return + + +def incorrectUsage(addr, mode): + if mode is None: + sendFailure(addr, "Incorrect usage") + return + if mode in main.help.keys(): + sendFailure(addr, "Usage: " + main.help[mode]) + return diff --git a/legacy/utils/parsing.py b/legacy/utils/parsing.py new file mode 100644 index 0000000..fb4f588 --- /dev/null +++ b/legacy/utils/parsing.py @@ -0,0 +1,15 @@ +def parsen(user): + step = user.split("!") + nick = step[0] + if len(step) == 2: + step2 = step[1].split("@") + if len(step2) == 2: + ident, host = step2 + else: + ident = nick + host = nick + else: + ident = nick + host = nick + + return (nick, ident, host)