get rid of the MessageDistributor module... & update discord, to work with API v10

Added discord commands (issue: as they're running over the event hub, they're processed on the same track as other events. => activity ingame triggers the next run; otherwise there's some delay for responses)
relay is a standard module now.
This commit is contained in:
2022-04-15 17:05:30 +02:00
parent fd84d82af2
commit a3a26f2ba4
32 changed files with 1036 additions and 729 deletions
@@ -0,0 +1,382 @@
import asyncio
import threading
import time
import re
from mailbox import Message
from typing import TYPE_CHECKING
from discord import Activity, ActivityType, Message, Embed
from core.aochat.BaseModule import BaseModule
from core.chat_blob import ChatBlob
from core.command_param_types import Const
from core.db import DB
from core.decorators import instance, command, event, timerevent
from core.dict_object import DictObject
from core.logger import Logger
from modules.core.ban.ban_service import BanService
if TYPE_CHECKING:
from modules.core.discord.discord_controller import DiscordController
from core.message_hub_service import MessageHubService
from core.command_service import CommandService
@instance()
class DiscordCommandHandler(BaseModule):
DISCORD_CHANNEL = "discord"
def __init__(self):
self.logger = Logger(__name__)
def inject(self, registry):
self.bot = registry.get_instance("bot")
self.discord: DiscordController = registry.get_instance("discord_controller")
self.db: DB = registry.get_instance("db")
self.relay_hub_service: MessageHubService = registry.get_instance("message_hub_service")
self.command_service: CommandService = registry.get_instance("command_service")
def pre_start(self):
self.command_service.register_command_channel("Discord", self.DISCORD_CHANNEL)
@event("discord_command", "should the bot take care of discord commands", False)
def discord_command_handler(self, _, event_data):
acc = event_data.account
ctx: Message = event_data.message
message = ctx.clean_content
if message.startswith(self.discord.setting_service.get("symbol").get_value()):
threading.Thread(target=self.command_service.process_command(
self.command_service.trim_command_symbol(message),
self.DISCORD_CHANNEL,
acc.main,
lambda msg: self.send_response(ctx, msg),
self.discord.bot.get_conn(),
False), daemon=True).start()
def send_response(self, ctx: Message, reply):
rsp = ""
embeds = []
if type(reply) == str:
reply = self.discord.text.format_message(reply)
rsp = f"> {self.parseDiscord(reply)}"
self.discord.relay_hub_service.send_message(f"Discord_({ctx.channel.name})",
DictObject({'char_id': 0, 'name': ctx.author.name,
'discord_handle':
f'{ctx.author.name}#{ctx.author.discriminator}'}),
reply, reply)
if type(reply) == ChatBlob:
if reply.embed:
embeds.append(Embed(title=self.parseDiscord(f"{reply.page_prefix} {reply.title} {reply.page_postfix}"),
color=0x00FF00,
description=self.parseDiscord(reply.msg).replace("\n> ", '\n')))
else:
reply: ChatBlob
rsp = self.parseDiscord(reply.page_prefix) + "\n"
if not (self.discord.text.strip_html_tags(reply.msg) or reply.msg).startswith(reply.title):
rsp += f"**__{reply.title}__**\n"
rsp += "> " + self.parseDiscord(reply.msg)
rsp += f"\n {self.parseDiscord(reply.page_postfix)}"
self.discord.relay_hub_service.send_message(f"Discord_({ctx.channel.name})",
DictObject({'char_id': 0, 'name': ctx.author.name,
'discord_handle':
f'{ctx.author.name}#{ctx.author.discriminator}'}),
reply, reply)
if len(rsp) > 2000:
rsp = self.discord.text.split_by_separators(rsp, 2000)
for page in rsp:
self.discord.client.loop.create_task(ctx.reply(page))
else:
self.discord.client.loop.create_task(ctx.reply(rsp, embeds=embeds))
def parseDiscord(self, ctx: str):
proficon = {1: "sold", 2: "ma", 3: "eng", 4: "fixer", 5: "agent", 6: "adv", 7: "trad", 8: "crat", 9: "enf",
10: "doc", 11: "nt", 12: "mp", 14: "keep", 15: "shade"}
m = re.findall(r"<img src='tdb://id:GFX_GUI_ICON_PROFESSION_(\d+)'>", ctx)
for match in m:
ctx = ctx.replace(f"<img src='tdb://id:GFX_GUI_ICON_PROFESSION_{match}'>",
f"#_{self.discord.util.get_profession(proficon.get(int(match)))}#_")
for x in ["`", ' *', ' _', ' |']:
ctx = ctx.replace(x, f' \\{x.strip()}')
for pattern, sub in [(r"(<br>|\n|</br>)", r"\n> "),
(r"<highlight>(.*?)</highlight>", r"**\1**"),
(r"<title>(.*?)</title>", r"**__\1__**"),
(r"<header>(.*?)</header>", r"**__\1__**"),
(r"<header2>(.*?)</header2>", r"__\1__"),
(r"<notice>(.*?)</notice>", r"*\1*"),
(r"<font color=('*?).+?\1>(.*?)</font>", r'\2'),
(r"<myname>", self.bot.get_char_name()),
(r"<symbol>", self.discord.setting_service.get_value("symbol")),
(r"<a href=(.*?)itemref://\d+/\d+/\d+\1>(.+?)<\/a>", r"`\2`"),
(r"<(.+?)>\s*?<\1>", ''),
(r"<pagebreak>", ''),
(r"<a.+?href=\'.*?\'>(.*?)</a>", r'`\1`'),
(r"<clan>(.*?)</clan>", r":yellow_circle: \1"),
(r"<omni>(.*?)</omni>", r":blue_circle: \1"),
(r"<neutral>(.*?)</neutral>", r":white_circle: \1"),
(r"<tab>", "\t"),
(r"<img src=(.?)rdb://\d+\1>", ''),
('#', ''),
('&lt;', '<'),
('&gt;', '>')]:
ctx = re.sub(pattern, sub, ctx)
cnt = 1
while cnt > 0:
ctx, cnt = re.subn(r"<(black|white|yellow|blue|green|red|orange|grey|cyan|violet)>(.*?|\d+?)</\1>", r"\2",
ctx)
ctx.rstrip()
ctx.rstrip(">")
return ctx
@command(command="discord", params=[Const("invite")], access_level="member",
description="Get a personal Discord invite", sub_command="invite")
def discord_invite_cmd(self, request, _):
if not self.discord.client:
return "Discord module has not been initiated yet. Please try again later."
account = self.discord.account_service.get_account(request.sender.char_id)
if account is None:
return "You do not have an account"
if account.disabled == 1:
return "Your account is disabled"
if account.discord_joined == 1:
return "You have already joined the Discord server"
if account.discord_invite != "":
for ginvite in self.discord.invites:
if ginvite.code == account.discord_invite:
asyncio.run_coroutine_threadsafe(self.discord.discord_delete_invite(ginvite), self.discord.loop)
invite = asyncio.run_coroutine_threadsafe(self.discord.discord_create_invite(account.name), self.discord.loop)
invite = invite.result()
self.discord.data.set_discord_invite(account.char_id, invite.code)
self.bot.send_mass_message(request.sender.char_id,
f"Your personal Discord invite is: {invite} \n"
f"This invite is only valid for 5 minutes.")
@command(command="discord", params=[Const("update")], access_level="member",
description="Update your discord information", sub_command="update")
def discord_update_cmd(self, request, _):
if not self.discord.client:
return "Discord module has not been initiated yet. Please try again later."
return self.discord.discord_update_account(request.sender.char_id)
@command(command="discord", params=[Const("disconnect")], access_level="admin",
description="Disconnect from Discord", sub_command="admin")
def discord_disconnect_cmd(self, _, _1):
if not self.discord.client:
return "Discord module has not been initiated yet. Please try again later."
if self.discord.client.is_closed():
return "Discord is already disconnected"
else:
asyncio.run_coroutine_threadsafe(self.discord.discord_disconnect(), self.discord.loop)
return "Disconnecting Discord"
@command(command="discord",
params=[Const("members")],
access_level="admin",
description="Get all discord members",
sub_command="members")
def discord_members_cmd(self, _, _1):
if not self.discord.client:
return "Discord module has not been initiated yet. Please try again later."
blob = ""
for member in self.discord.guild.members:
member_roles = []
for role in member.roles:
if role.name == "@everyone": # Skip @everyone as everyone has it.
continue
member_roles.append(f"{role.name}")
member_roles.sort(key=str.lower)
blob += f"{member.name + '#' + member.discriminator} ({', '.join(member_roles)})\n"
return ChatBlob(f"Discord Members ({len(self.discord.guild.members):d})", blob)
@event(event_type="connect", description="Connects the Discord client automatically on startup, if a token exists")
def handle_connect_event(self, _, _1):
token = self.discord.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
# noinspection PyTypeChecker
self.discord.loop = asyncio.get_event_loop()
self.discord.loop.create_task(self.discord.discord_connect(token))
self.discord.thread = threading.Thread(target=self.discord.run_it_forever, daemon=True)
self.discord.thread.start()
@event(event_type=BanService.BAN_ADDED_EVENT, description="Ban user from Discord")
def ban_added_event(self, _, event_data):
token = self.discord.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
account = self.discord.account_service.get_account(event_data.char_id)
if account.discord_joined == 1 and account.discord_id != "":
member = self.discord.guild.get_member(int(account.discord_id))
if member is not None:
ban = asyncio.run_coroutine_threadsafe(self.discord.discord_ban_user(member, event_data.reason),
self.discord.client.loop)
@event(event_type=BanService.BAN_REMOVED_EVENT, description="Remove Discord ban")
def ban_removed_event(self, _, event_data):
token = self.discord.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
account = self.discord.account_service.get_account(event_data.char_id)
if account.discord_id != "":
banlist = asyncio.run_coroutine_threadsafe(self.discord.discord_banlist(), self.discord.client.loop)
banlist = banlist.result()
for ban in banlist:
if ban.user.id == int(account.discord_id):
unban = asyncio.run_coroutine_threadsafe(self.discord.discord_unban_user(ban.user),
self.discord.client.loop)
@timerevent(budatime="1s", description="Handle Discord queue")
def timer_check_discord_queue(self, _, _1):
while self.discord.discord_queue:
t = int(time.time())
obj = self.discord.discord_queue.pop(0)
if obj.type == "on_member_remove":
member = obj.member
handle = member.name + "#" + member.discriminator
account = self.discord.data.get_account_discord_id(member.id)
if not account:
log = '**%s** has left discord (**%s**)' % (member.nick or member.name, handle)
self.relay_hub_service.send_message("system_logger", 0, log, log)
self.logger.info(log)
continue
main = self.discord.account_service.get_main(account.char_id)
if account is None:
continue
log = '**%s** has left discord (**%s**)' % (main.name, handle)
self.relay_hub_service.send_message("system_logger", 0, log, log)
self.logger.info(log)
# self.bot.send_private_channel_message("%s has left Discord (%s)" % (main.name, handle))
self.discord.data.set_discord_left(account.main)
if account.discord_handle != handle:
self.discord.data.set_discord_handle(member.id, handle)
if obj.type == "on_member_join":
member = obj.member
invite_used = obj.invite
handle = member.name + "#" + member.discriminator
if invite_used is None:
log = '**%s** joined discord with unknown invite' % handle
self.relay_hub_service. \
send_message("system_logger", 0,
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!",
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!")
self.logger.info(log)
continue
self.discord.guild = self.discord.client.get_guild(self.discord.client.guilds[0].id)
account = self.discord.data.get_discord_invite(invite_used.code)
if account is None:
log = '**%s** joined discord with invite **%s** but couldnt find account!' % (
handle, invite_used.code)
self.relay_hub_service. \
send_message("system_logger", 0,
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!",
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!")
self.logger.info(log)
asyncio.run_coroutine_threadsafe(self.discord.discord_member_roles(member, None, None),
self.discord.loop)
continue
main = self.discord.account_service.get_main(account.main)
self.discord.data.set_discord_joined(account.main, handle, member.id)
if self.discord.setting_service.get_value('is_alliance_bot') == '1':
nick = f"{f'[{self.discord.alias_controller.get_alias(main.org_id)}] '}{main.name}"
else:
nick = f"{main.name}"
nick = asyncio.run_coroutine_threadsafe(self.discord.discord_member_nick(member, nick),
self.discord.loop)
access_level = access_level = self.discord.access_service.get_access_level(account.main)
roles = asyncio.run_coroutine_threadsafe(
self.discord.discord_member_roles(member, account, access_level),
self.discord.loop)
# noinspection LongLine
log = '**%s** joined discord with invite **%s** and matches account **%s**' \
% (handle, invite_used.code,
f"{f'[{self.discord.alias_controller.get_alias(main.org_id)}] ' if self.discord.setting_service.get_value('is_alliance_bot') == '1' else ''}{main.name}")
self.relay_hub_service.send_message("system_logger", account.main, log, log)
self.logger.info(log)
@timerevent(budatime="1h", description="Verify Discord members", run_at_startup=True)
def timer_check_discord_members(self, event_type, event_data):
token = self.discord.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
if not self.bot.is_ready():
return
update = asyncio.run_coroutine_threadsafe(self.discord.discord_update_bot_basic(), self.discord.loop)
# Update accounts that have left/joined discord without the bot being online
accounts = self.db.query("SELECT * FROM account WHERE discord_id !=''")
for account in accounts:
match = False
for member in self.discord.guild.members:
handle = member.name + "#" + member.discriminator
if member.id == int(account.discord_id):
match = True
if account.discord_joined == 0:
self.discord.data.set_discord_joined(account.main, handle, member.id)
break
if match is False:
if account.discord_joined == 1:
self.discord.data.set_discord_left(account.main)
# Update current discord Members
accounts = self.discord.db.query("SELECT * FROM account WHERE discord_joined = 1 and char_id = main")
for member in self.discord.guild.members:
if member.id == self.discord.client.user.id:
continue
member_account = None
for account in list(accounts):
if int(account.discord_id) == member.id:
member_account = account
accounts.remove(account)
break
access_level = 0
if member_account is not None:
access_level = self.discord.access_service.get_access_level(member_account.main)
roles = asyncio.run_coroutine_threadsafe(
self.discord.discord_member_roles(member, member_account, access_level),
self.discord.loop)
if member_account is not None:
main = self.discord.pork.get_character_info(member_account.main)
# noinspection LongLine
nick = f"{f'[{self.discord.alias_controller.get_alias(main.org_id)}] ' if self.discord.setting_service.get_value('is_alliance_bot') == '1' else ''}{main.name}"
nick = asyncio.run_coroutine_threadsafe(self.discord.discord_member_nick(member, nick),
self.discord.loop)
@event(event_type="main_changed", description="Fix discord names & ranks")
def fix_ranks(self, _, data):
row = self.db.query_single("SELECT * from account where char_id=?", [data.old_main_id])
if row.discord_joined == 0:
self.logger.debug(f"{data.old_main_id} was not in discord, ignoring main fixing")
return
elif row.discord_joined == 1:
self.db.exec(
"UPDATE account set discord_handle=?, discord_id=?, discord_invite=?, discord_joined=? where char_id=?",
[row.discord_handle, row.discord_id, row.discord_invite, row.discord_joined, data.new_main_id])
self.db.exec(
"UPDATE account set discord_handle='', discord_id=0, discord_invite='', discord_joined=0 "
"where char_id=?",
[data.old_main_id])
self.logger.info(f"{data.old_main_id} was in discord, overwriting {data.new_main_id} with {data}")
self.discord.discord_update_account(data.new_main_id)
@timerevent(budatime="5m", description="update activity")
def change_count(self, _, _1):
if hasattr(self, "loop"):
count = self.db.query_single('SELECT count(*) as count from online '
'where char_id NOT IN (select char_id from org_bots) and bot=?',
[self.bot.get_char_id()]).count
act = Activity(type=ActivityType.listening,
name=f"{count} Players")
asyncio.run_coroutine_threadsafe(self.discord.client.change_presence(activity=act), self.discord.loop)
+86 -388
View File
@@ -1,36 +1,38 @@
import asyncio
import html
import logging
import re
import threading
import time
from asyncio import BaseEventLoop
from datetime import datetime, timedelta
from functools import partial
from html.parser import HTMLParser
# noinspection PyPackageRequirements
import discord
import emojis as emojis
# noinspection PyPackageRequirements
from discord import Message, TextChannel, Guild, Embed, Role
from typing import TYPE_CHECKING
# noinspection PyPackageRequirements
import discord
from discord import Message, TextChannel, Embed, Role, Guild
from core.chat_blob import ChatBlob
from core.command_param_types import Const
from core.db import DB
from core.decorators import instance, command, event, timerevent
from core.decorators import instance, event, timerevent
from core.dict_object import DictObject
from core.logger import Logger
from core.lookup.character_service import CharacterService
from core.lookup.pork_service import PorkService
from core.message_hub_service import MessageHubService
from core.setting_service import SettingService
from core.setting_types import HiddenSettingType
from core.setting_types import HiddenSettingType, NumberSettingType, TextSettingType, BooleanSettingType
from core.text import Text
from core.igncore import IgnCore
from core.util import Util
from modules.core.accounting.services.access_service import AccessService
from modules.core.accounting.services.account_service import AccountService
from modules.core.ban.ban_service import BanService
from modules.onlinebot.online.org_alias_controller import OrgAliasController
from modules.core.discord.discord_data import DiscordData
from modules.core.discord.discord_management import DiscordManager
if TYPE_CHECKING:
from modules.core.accounting.services.access_service import AccessService
from modules.core.accounting.services.account_service import AccountService
from modules.onlinebot.online.org_alias_controller import OrgAliasController
from modules.core.discord.discord_command_handler import DiscordCommandHandler
from core.lookup.character_service import CharacterService
from core.lookup.pork_service import PorkService
from core.message_hub_service import MessageHubService
from core.setting_service import SettingService
class MLStripper(HTMLParser):
@@ -61,18 +63,11 @@ class DiscordController:
logging.getLogger("discord.gateway").setLevel(logging.WARN)
logging.getLogger("discord.client").setLevel(logging.WARN)
intents = discord.Intents.all()
intents.message_content = True
self.client = discord.Client(intents=intents, chunk_guilds_at_startup=True)
self.client.event(self.on_ready)
self.client.event(self.on_member_join)
self.client.event(self.on_member_remove)
self.client.event(self.on_member_update)
self.client.event(self.on_guild_role_delete)
self.client.event(self.on_guild_role_create)
self.client.event(self.on_guild_role_update)
self.client.event(self.on_invite_create)
self.client.event(self.on_invite_delete)
self.manager: DiscordManager = DiscordManager(self)
self.client.event(self.on_message)
self.guild = None
self.guild: Guild = None
self.channel = None
self.thread = None
self.invites = None
@@ -106,9 +101,17 @@ class DiscordController:
self.account_service: AccountService = registry.get_instance("account_service")
self.setting_service: SettingService = registry.get_instance("setting_service")
self.data: DiscordData = DiscordData(self.db, self.client)
self.cmd: DiscordCommandHandler = registry.get_instance("discord_command_handler")
def pre_start(self):
self.setting_service.register(self.module_name, "discord_token", "", HiddenSettingType(allow_empty=True),
"Enter your Discord token here")
self.setting_service.register(self.module_name, "public_relay", "[0]", TextSettingType(),
"Public discord relay (purpose: autodelete messages after 1h)")
self.setting_service.register(self.module_name, "channel_blacklist", "[0]", TextSettingType(),
"Channels which should not trigger the command handler")
self.bot.event_service.register_event_type("discord_command")
def get_name(self, discord_id):
data = self.db.query_single(
@@ -118,242 +121,9 @@ class DiscordController:
def setting_discord_token(self):
return self.setting_service.get("discord_token")
@command(command="discord", params=[Const("invite")], access_level="member",
description="Get a personal Discord invite", sub_command="invite")
def discord_invite_cmd(self, request, _):
if not self.client:
return "Discord module has not been initiated yet. Please try again later."
account = self.account_service.get_account(request.sender.char_id)
if account is None:
return "You do not have an account"
if account.disabled == 1:
return "Your account is disabled"
if account.discord_joined == 1:
return "You have already joined the Discord server"
if account.discord_invite != "":
for ginvite in self.invites:
if ginvite.code == account.discord_invite:
asyncio.run_coroutine_threadsafe(self.discord_delete_invite(ginvite), self.loop)
invite = asyncio.run_coroutine_threadsafe(self.discord_create_invite(account.name), self.loop)
invite = invite.result()
self.set_discord_invite(account.char_id, invite.code)
self.bot.send_mass_message(request.sender.char_id,
f"Your personal Discord invite is: {invite} \n"
f"This invite is only valid for 5 minutes.")
@command(command="discord", params=[Const("update")], access_level="member",
description="Update your discord information", sub_command="update")
def discord_update_cmd(self, request, _):
if not self.client:
return "Discord module has not been initiated yet. Please try again later."
return self.discord_update_account(request.sender.char_id)
@command(command="discord", params=[Const("disconnect")], access_level="admin",
description="Disconnect from Discord", sub_command="admin")
def discord_disconnect_cmd(self, _, _1):
if not self.client:
return "Discord module has not been initiated yet. Please try again later."
if self.client.is_closed():
return "Discord is already disconnected"
else:
asyncio.run_coroutine_threadsafe(self.discord_disconnect(), self.loop)
return "Disconnecting Discord"
@command(command="discord",
params=[Const("members")],
access_level="admin",
description="Get all discord members",
sub_command="members")
def discord_members_cmd(self, _, _1):
if not self.client:
return "Discord module has not been initiated yet. Please try again later."
blob = ""
for member in self.guild.members:
member_roles = []
for role in member.roles:
if role.name == "@everyone": # Skip @everyone as everyone has it.
continue
member_roles.append("%s" % role.name)
member_roles.sort(key=str.lower)
blob += "%s (%s)\n" % (member.name + "#" + member.discriminator, ", ".join(member_roles))
return ChatBlob("Discord Members (%d)" % (len(self.guild.members)), blob)
@event(event_type="connect", description="Connects the Discord client automatically on startup, if a token exists")
def handle_connect_event(self, _, _1):
token = self.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
# noinspection PyTypeChecker
self.loop: BaseEventLoop = asyncio.get_event_loop()
self.loop.create_task(self.discord_connect(token))
self.thread = threading.Thread(target=self.run_it_forever, daemon=True)
self.thread.start()
@event(event_type=BanService.BAN_ADDED_EVENT, description="Ban user from Discord")
def ban_added_event(self, _, event_data):
token = self.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
account = self.account_service.get_account(event_data.char_id)
if account.discord_joined == 1 and account.discord_id != "":
member = self.guild.get_member(int(account.discord_id))
if member is not None:
ban = asyncio.run_coroutine_threadsafe(self.discord_ban_user(member, event_data.reason), self.loop)
@event(event_type=BanService.BAN_REMOVED_EVENT, description="Remove Discord ban")
def ban_removed_event(self, _, event_data):
token = self.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
account = self.account_service.get_account(event_data.char_id)
if account.discord_id != "":
banlist = asyncio.run_coroutine_threadsafe(self.discord_banlist(), self.loop)
banlist = banlist.result()
for ban in banlist:
if ban.user.id == int(account.discord_id):
unban = asyncio.run_coroutine_threadsafe(self.discord_unban_user(ban.user), self.loop)
@timerevent(budatime="1s", description="Handle Discord queue")
def timer_check_discord_queue(self, _, _1):
while self.discord_queue:
t = int(time.time())
obj = self.discord_queue.pop(0)
if obj.type == "on_member_remove":
member = obj.member
handle = member.name + "#" + member.discriminator
account = self.get_account_discord_id(member.id)
if not account:
log = '**%s** has left discord (**%s**)' % (member.nick or member.name, handle)
self.relay_hub_service.send_message("system_logger", 0, log, log)
self.logger.info(log)
continue
main = self.account_service.get_main(account.char_id)
if account is None:
continue
log = '**%s** has left discord (**%s**)' % (main.name, handle)
self.relay_hub_service.send_message("system_logger", 0, log, log)
self.logger.info(log)
# self.bot.send_private_channel_message("%s has left Discord (%s)" % (main.name, handle))
self.set_discord_left(account.main)
if account.discord_handle != handle:
self.set_discord_handle(member.id, handle)
if obj.type == "on_member_join":
member = obj.member
invite_used = obj.invite
handle = member.name + "#" + member.discriminator
if invite_used is None:
log = '**%s** joined discord with unknown invite' % handle
self.relay_hub_service. \
send_message("system_logger", 0,
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!",
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!")
self.logger.info(log)
continue
self.guild: Guild = self.client.get_guild(self.client.guilds[0].id)
account = self.get_discord_invite(invite_used.code)
if account is None:
log = '**%s** joined discord with invite **%s** but couldnt find account!' % (
handle, invite_used.code)
self.relay_hub_service. \
send_message("system_logger", 0,
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!",
log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
f"check that!")
self.logger.info(log)
asyncio.run_coroutine_threadsafe(self.discord_member_roles(member, None, None), self.loop)
continue
main = self.account_service.get_main(account.main)
self.set_discord_joined(account.main, handle, member.id)
if self.setting_service.get_value('is_alliance_bot') == '1':
nick = f"{f'[{self.alias_controller.get_alias(main.org_id)}] '}{main.name}"
else:
nick = f"{main.name}"
nick = asyncio.run_coroutine_threadsafe(self.discord_member_nick(member, nick), self.loop)
access_level = access_level = self.access_service.get_access_level(account.main)
roles = asyncio.run_coroutine_threadsafe(self.discord_member_roles(member, account, access_level),
self.loop)
# noinspection LongLine
log = '**%s** joined discord with invite **%s** and matches account **%s**' \
% (handle, invite_used.code,
f"{f'[{self.alias_controller.get_alias(main.org_id)}] ' if self.setting_service.get_value('is_alliance_bot') == '1' else ''}{main.name}")
self.relay_hub_service.send_message("system_logger", account.main, log, log)
self.logger.info(log)
@timerevent(budatime="1h", description="Verify Discord members", run_at_startup=True)
def timer_check_discord_members(self, event_type, event_data):
token = self.setting_discord_token().get_value()
if token.lower() in ["none", "", "null"]:
return
if not self.bot.is_ready():
return
update = asyncio.run_coroutine_threadsafe(self.discord_update_bot_basic(), self.loop)
# Update accounts that have left/joined discord without the bot being online
accounts = self.db.query("SELECT * FROM account WHERE discord_id !=''")
for account in accounts:
match = False
for member in self.guild.members:
handle = member.name + "#" + member.discriminator
if member.id == int(account.discord_id):
match = True
if account.discord_joined == 0:
self.set_discord_joined(account.main, handle, member.id)
break
if match is False:
if account.discord_joined == 1:
self.set_discord_left(account.main)
# Update current discord Members
accounts = self.db.query("SELECT * FROM account WHERE discord_joined = 1 and char_id = main")
for member in self.guild.members:
if member.id == self.client.user.id:
continue
member_account = None
for account in list(accounts):
if int(account.discord_id) == member.id:
member_account = account
accounts.remove(account)
break
access_level = 0
if member_account is not None:
access_level = self.access_service.get_access_level(member_account.main)
roles = asyncio.run_coroutine_threadsafe(self.discord_member_roles(member, member_account, access_level),
self.loop)
if member_account is not None:
main = self.pork.get_character_info(member_account.main)
# noinspection LongLine
nick = f"{f'[{self.alias_controller.get_alias(main.org_id)}] ' if self.setting_service.get_value('is_alliance_bot') == '1' else ''}{main.name}"
nick = asyncio.run_coroutine_threadsafe(self.discord_member_nick(member, nick), self.loop)
def run_it_forever(self):
self.loop.run_forever()
def set_discord_invite(self, char_id, invite):
return self.db.exec("UPDATE account SET discord_invite = ? WHERE main = ?", [invite, char_id])
def set_discord_joined(self, char_id, handle, discord_id):
return self.db.exec("UPDATE account SET discord_joined = 1, discord_handle = ?, discord_id = ? WHERE main = ?",
[handle, discord_id, char_id])
def set_discord_left(self, char_id):
return self.db.exec("UPDATE account SET discord_joined = 0 WHERE main = ?", [char_id])
def set_discord_handle(self, discord_id, handle):
return self.db.exec("UPDATE account SET discord_handle = ? WHERE discord_id = ?", [handle, discord_id])
def get_discord_invite(self, invite):
return self.db.query_single("SELECT * FROM account WHERE discord_invite = ?", [invite])
def get_account_discord_id(self, discord_id):
return self.db.query_single(
"SELECT * FROM account a left join player p on a.char_id=p.char_id WHERE discord_id = ?", [discord_id])
def discord_update_account(self, char_id):
account = self.account_service.get_account(char_id)
if account is None:
@@ -371,65 +141,12 @@ class DiscordController:
return "Processing..."
return "No Discord account found"
def discord_invite_used(self, temp_invites):
for temp_invite in temp_invites:
# if temp_invite.inviter.id == self.client.user.id:
# ## Using this line limits it to invite created by the account the Discord bot runs as..
for invite in self.invites:
if int(temp_invite.uses) > int(invite.uses) and temp_invite.code == invite.code:
return temp_invite
return None
def get_role(self, name, roles) -> Role or None:
for role in roles:
if role.name == name:
return role
return None
async def on_member_join(self, member):
temp_invites = await self.guild.invites()
invite_used = self.discord_invite_used(temp_invites)
self.discord_queue.append(DictObject({"type": "on_member_join", "member": member, "invite": invite_used}))
await invite_used.delete()
async def on_member_remove(self, member):
self.discord_queue.append(DictObject({"type": "on_member_remove", "member": member}))
await self.discord_update_bot_full()
async def on_ready(self):
await self.discord_update_bot_full()
count = self.db.query_single('SELECT count(*) as count from online '
'where char_id NOT IN (select char_id from org_bots) and bot=?',
[self.bot.get_char_id()]).count
act = discord.Activity(type=discord.ActivityType.listening,
name=f"{count} Players")
asyncio.run_coroutine_threadsafe(self.client.change_presence(activity=act), self.loop)
await self.clean_channel()
if self.guild.large:
self.logger.error(
f"Guild {self.guild.name} is classified as large, "
f"you need to request offline members to manage roles properly, this is not yet implemented")
async def on_member_update(self, member_before, member_after):
await self.discord_update_bot_basic()
async def on_guild_role_create(self, role):
await self.discord_update_bot_full()
async def on_guild_role_delete(self, role):
await self.discord_update_bot_full()
async def on_guild_role_update(self, role_before, role_after):
await self.discord_update_bot_full()
async def on_invite_create(self, invite):
await self.discord_update_bot_full()
async def on_invite_delete(self, invite):
await self.discord_update_bot_full()
async def discord_update_bot_basic(self):
self.guild = self.client.get_guild(self.client.guilds[0].id)
self.channel = self.client.get_channel(self.guild.text_channels[0].id)
@@ -448,7 +165,37 @@ class DiscordController:
await self.client.start(token)
except discord.ClientException as exc:
self.logger.error("Something broke, I'm out!: %s" % str(exc))
self.logger.error(f"Something broke, I'm out!: {str(exc)}")
async def setup_relayhub(self):
for channel in self.guild.channels:
if type(channel) == TextChannel:
channel: TextChannel
self.relay_hub_service.register_message_source(f"Discord_({channel.name.replace(' ', '_')})")
self.relay_hub_service.register_message_destination(f"Discord_({channel.name.replace(' ', '_')})",
partial(self.relay, channel.id), [], [f"Discord_({channel.name.replace(' ', '_')})"])
def relay(self, channel, ctx):
ch: TextChannel = self.guild.get_channel(channel)
msg = ctx.formatted_message
del_after = 3600 if f"{ch.id}" in self.setting_service.get_value("public_relay").lstrip("[").rstrip("]").split(',') else None
if ctx.source == "alliance":
msg = self.cmd.parseDiscord(ctx.message)
elif type(msg) == ChatBlob:
if msg.embed:
msg: ChatBlob
blob = Embed(title=msg.title, color=0x00FF00, description=self.cmd.parseDiscord(msg.msg).replace("\n> ", '\n'))
asyncio.run_coroutine_threadsafe(ch.send("", delete_after=del_after, embed=blob), self.loop)
return
rsp = self.cmd.parseDiscord(ctx.formatted_message.page_prefix) + "\n"
if not self.text.strip_html_tags(ctx.formatted_message.msg).startswith(ctx.formatted_message.title):
rsp += f"**__{ctx.formatted_message.title}__**\n"
rsp += "> " + self.cmd.parseDiscord(ctx.formatted_message.msg)
rsp += f"\n {self.cmd.parseDiscord(ctx.formatted_message.page_postfix)}"
msg = rsp
else:
msg = self.cmd.parseDiscord(ctx.formatted_message)
msg: Message = asyncio.run_coroutine_threadsafe(ch.send(msg, delete_after=del_after), self.loop)
async def discord_create_invite(self, reason=""):
created_invite = await self.channel.create_invite(max_age=300, max_uses=2, reason=reason)
@@ -578,28 +325,29 @@ class DiscordController:
await discord_user.remove_roles(role)
async def on_message(self, msg: Message):
if f"{msg.channel.id}" in self.setting_service.get_value("public_relay").lstrip("[").rstrip("]").split(','):
await msg.delete(delay=3600)
if msg.author.id == self.client.user.id:
return
if not self.setting_service.get_value("dc_relay_public"):
return
channel: TextChannel = msg.channel
if channel.id == int(self.setting_service.get_value("dc_relay_public")):
if self.setting_service.get_value('is_alliance_bot') == "0":
response = f"[<notice>{html.escape(msg.author.nick if msg.author.nick else msg.author.name, False)}" \
f"</notice>]: " \
f"{html.escape(emojis.decode(msg.clean_content), False)}"
else:
response = f"{html.escape(msg.author.nick if msg.author.nick else msg.author.name, False)}: " \
f"{html.escape(emojis.decode(msg.clean_content), False)}"
await msg.delete(delay=3600)
sender = self.db.query_single("SELECT * from account a left join player p on a.main = p.char_id where a.discord_id = ?", [msg.author.id])
self.relay_hub_service.send_message("public_relay", sender, html.escape(emojis.decode(msg.clean_content), False), response)
acc = self.data.get_account_discord_id(msg.author.id)
if acc:
acc.discord_nick = msg.author.nick or msg.author.name+'#'+msg.author.discriminator
prefix = f"[{self.alias_controller.get_alias(acc.member)}] {acc.name}: " if self.alias_usage().get_value() else f"[DC] {acc.name}: "
self.relay_hub_service.send_message("Discord_(" + channel.name+")",
DictObject(acc), msg.content,
prefix + msg.content)
else:
prefix = f"[-UNK-] {msg.author.username}: " if self.alias_usage().get_value() else f"[DC] {msg.author.username}: "
self.relay_hub_service.send_message("Discord_(" + channel.name+")",
DictObject({'char_id': 0, 'name': msg.author.username, 'discord_handle': f'{msg.author.name}#{msg.author.discriminator}'}), msg.content,
prefix + msg.content)
if str(channel.id) not in self.channel_blacklist().get_value().lstrip("[").rstrip("]").split(','):
self.bot.event_service.fire_event("discord_command", DictObject({'account': acc, 'message': msg}))
if self.is_command(msg.content):
admin = self.get_role("Administrator", self.guild.roles)
council = self.get_role("Council", self.guild.roles)
if msg.content[:4] == "!pin":
if admin in msg.author.roles:
matches = re.findall(pattern="<head>(.+?)<\/head>(.+)", string=msg.content[4:].strip(),
@@ -611,7 +359,7 @@ class DiscordController:
mess = await channel.send(embed=Embed(color=3066993, description=msg.content[4:]))
await mess.pin()
if msg.content[:5] == "!note":
if msg.content[:6] == "!note ":
if admin in msg.author.roles:
matches = re.findall(pattern="<head>(.+?)<\/head>(.+)", string=msg.content[5:].strip(),
flags=re.DOTALL)
@@ -636,73 +384,23 @@ class DiscordController:
else:
await msg.channel.purge(check=self.check, limit=count)
await msg.delete(delay=0)
if msg.content[:10] == "!subscribe":
if admin in msg.author.roles:
out = msg.content[10:].strip()
if self.setting_service.get(out) is None:
await msg.reply(f"The Channel {out} does not exist.")
else:
self.setting_service.set_value(out, msg.channel.id)
self.send_message(msg.channel.id, Embed(color=3066993, title="Channel Guard",
description=f"This channel has been "
f"subscribed to the source {out}."))
def send_message(self, channel: int, message, delete=0):
if self.client.is_ready():
channel: TextChannel = self.client.get_channel(int(channel)) if type(channel) in [int, str] else channel
if type(message) == Embed:
if delete != 0:
asyncio.run_coroutine_threadsafe(channel.send(embed=message, delete_after=delete), self.loop)
else:
asyncio.run_coroutine_threadsafe(channel.send(embed=message), self.loop)
else:
if delete != 0:
asyncio.run_coroutine_threadsafe(channel.send(message, delete_after=delete), self.loop)
else:
asyncio.run_coroutine_threadsafe(channel.send(message), self.loop)
def is_command(self, message: str):
for x in ["subscribe", "pin", "purge", "note"]:
if message.startswith("!" + x):
return True
async def clean_channel(self):
if self.setting_service.get_value("dc_relay_public") not in ["0", None]:
channel: TextChannel = self.client.get_channel(int(self.setting_service.get_value("dc_relay_public")))
for i in range(5):
await channel.purge(check=self.check)
def check(self, msg: Message):
if msg.pinned or len(msg.embeds) > 0:
if msg.pinned or (len(msg.embeds) > 0 and (msg.created_at.utcnow() < datetime.utcnow()-timedelta(hours=12))):
return False
else:
return True
@event(event_type="main_changed", description="Fix discord names & ranks")
def fix_ranks(self, _, data):
row = self.db.query_single("SELECT * from account where char_id=?", [data.old_main_id])
if row.discord_joined == 0:
self.logger.debug(f"{data.old_main_id} was not in discord, ignoring main fixing")
return
elif row.discord_joined == 1:
self.db.exec(
"UPDATE account set discord_handle=?, discord_id=?, discord_invite=?, discord_joined=? where char_id=?",
[row.discord_handle, row.discord_id, row.discord_invite, row.discord_joined, data.new_main_id])
self.db.exec(
"UPDATE account set discord_handle='', discord_id=0, discord_invite='', discord_joined=0 "
"where char_id=?",
[data.old_main_id])
self.logger.info(f"{data.old_main_id} was in discord, overwriting {data.new_main_id} with {data}")
def alias_usage(self) -> BooleanSettingType:
return self.setting_service.get("is_alliance_bot")
self.discord_update_account(data.new_main_id)
def channel_blacklist(self) -> TextSettingType:
return self.setting_service.get("channel_blacklist")
@timerevent(budatime="5m", description="update activity")
def change_count(self, _, _1):
if hasattr(self, "loop"):
count = self.db.query_single('SELECT count(*) as count from online '
'where char_id NOT IN (select char_id from org_bots) and bot=?',
[self.bot.get_char_id()]).count
act = discord.Activity(type=discord.ActivityType.listening,
name=f"{count} Players")
asyncio.run_coroutine_threadsafe(self.client.change_presence(activity=act), self.loop)
def public_relay(self) -> TextSettingType:
return self.setting_service.get("public_relay")
+25
View File
@@ -0,0 +1,25 @@
class DiscordData:
def __init__(self, db, client):
self.db = db
self.client = client
def set_discord_invite(self, char_id, invite):
return self.db.exec("UPDATE account SET discord_invite = ? WHERE main = ?", [invite, char_id])
def set_discord_joined(self, char_id, handle, discord_id):
return self.db.exec("UPDATE account SET discord_joined = 1, discord_handle = ?, discord_id = ? WHERE main = ?",
[handle, discord_id, char_id])
def set_discord_left(self, char_id):
return self.db.exec("UPDATE account SET discord_joined = 0 WHERE main = ?", [char_id])
def set_discord_handle(self, discord_id, handle):
return self.db.exec("UPDATE account SET discord_handle = ? WHERE discord_id = ?", [handle, discord_id])
def get_discord_invite(self, invite):
return self.db.query_single("SELECT * FROM account WHERE discord_invite = ?", [invite])
def get_account_discord_id(self, discord_id):
return self.db.query_single(
"SELECT * FROM account a left join player p on a.char_id=p.char_id WHERE discord_id = ?", [discord_id])
@@ -0,0 +1,82 @@
import asyncio
from discord import Activity, ActivityType, TextChannel
from core.dict_object import DictObject
class DiscordManager:
def __init__(self, client):
self.client = client
client = client.client
client.event(self.on_ready)
client.event(self.on_member_join)
client.event(self.on_member_remove)
client.event(self.on_member_update)
client.event(self.on_guild_role_delete)
client.event(self.on_guild_role_create)
client.event(self.on_guild_role_update)
client.event(self.on_invite_create)
client.event(self.on_invite_delete)
async def on_member_update(self, member_before, member_after):
await self.client.discord_update_bot_basic()
async def on_guild_role_create(self, role):
await self.client.discord_update_bot_full()
async def on_guild_role_delete(self, role):
await self.client.discord_update_bot_full()
async def on_guild_role_update(self, role_before, role_after):
await self.client.discord_update_bot_full()
async def on_invite_create(self, invite):
await self.client.discord_update_bot_full()
async def on_invite_delete(self, invite):
await self.client.discord_update_bot_full()
async def on_member_join(self, member):
temp_invites = await self.client.guild.invites()
invite_used = self.discord_invite_used(temp_invites)
self.client.discord_queue.append(
DictObject({"type": "on_member_join", "member": member, "invite": invite_used}))
await invite_used.delete()
def discord_invite_used(self, temp_invites):
for temp_invite in temp_invites:
# if temp_invite.inviter.id == self.client.user.id:
# ## Using this line limits it to invite created by the account the Discord bot runs as..
for invite in self.client.invites:
if int(temp_invite.uses) > int(invite.uses) and temp_invite.code == invite.code:
return temp_invite
return None
async def on_member_remove(self, member):
self.client.discord_queue.append(DictObject({"type": "on_member_remove", "member": member}))
await self.client.discord_update_bot_full()
async def on_ready(self):
await self.client.discord_update_bot_full()
count = self.client.db.query_single('SELECT count(*) as count from online '
'where char_id NOT IN (select char_id from org_bots) and bot=?',
[self.client.bot.get_char_id()]).count
act = Activity(type=ActivityType.listening,
name=f"{count} Players")
asyncio.run_coroutine_threadsafe(self.client.client.change_presence(activity=act), self.client.loop)
asyncio.run_coroutine_threadsafe(self.client.setup_relayhub(), self.client.loop)
await self.clean_channel()
if self.client.guild.large:
self.client.logger.error(
f"Guild {self.client.guild.name} is classified as large, "
f"you need to request offline members to manage roles properly, this is not yet implemented")
async def clean_channel(self):
for channel in self.client.setting_service.get_value("public_relay").lstrip("[").rstrip("]").split(','):
if ch := self.client.client.get_channel(int(channel)):
for i in range(5):
await ch.purge(check=self.client.check)