diff --git a/.coveragerc b/.coveragerc index 2f36419..b2cd32d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,4 +5,5 @@ include = [report] exclude_lines = pragma: no cover - if __name__ == .__main__.: \ No newline at end of file + if __name__ == .__main__.: + __repr__ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2517295..ed25611 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # User files config.json +config.yml *.data # IDEs diff --git a/CHANGELOG.md b/CHANGELOG.md index 57eb711..4a55cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ # Changelog +## Version 1.0.0 (2019-07-09) +- Now using YAML instead of json for configuration. +- Now announces guildhall changes. +- Interval between scans is now configurable. +- Removed some configurable values that made the config file more complex. + ## Version 0.2.0 (2018-08-24) - GuildWatcher can now detect invites - Announces when a new character is invited diff --git a/README.md b/README.md index 9738622..ecbc1fa 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,11 @@ pip install -r requirements.txt 1. Click on **Create Webhook**. 1. Customize the avatar as needed. 1. Copy the webhook's URL. -1. Create a file named **config.json** and edit it, basing it on **config-example.json**. +1. Create a file named **config.yml** and edit it, basing it on **config-example.yml**. * The top level `webhook_url` will be used, but if you want another guild to use a different URL, you can specify one for that guild. - * If `override_image` is added to the guild, its logo will be used instead. ## Running the script -- `config.json` must be in the same directory you're running the script from. +- `config.yml` must be in the same directory you're running the script from. - The script generates `.data` files, named after the guilds, these save the last state of the guild, to compare it with the current state. If installed using pip, you can run the script in one of two ways: @@ -53,22 +52,26 @@ python -m guildwatcher ``` ## Current Features -* Announces when a member joins. -* Announces when a member leaves or is kicked. -* Announce when a member is promoted or demoted. -* Announce when a member changes name. -* Announce when a member's title is changed. -* Announce when a new character is invited. -* Announce when an invitation is revoked or rejected. -* Multiple guilds support. -* Webhook URL configurable per guild. +- Announces when a member joins. +- Announces when a member leaves or is kicked. +- Announce when a member is promoted or demoted. +- Announce when a member changes name. +- Announce when a member's title is changed. +- Announce when a new character is invited. +- Announce when an invitation is revoked or rejected. +- Announce when the guildhall changes. +- Multiple guilds support. +- Configurable scan times. +- Webhook URL configurable per guild. ## Known Issues -* Renaming a rank would trigger all rank members getting announced as leaving and joining back. +- Renaming a rank would trigger all rank members getting announced as leaving and joining back. ## Planned features -* Configurable scan times. -* Announce changes in guild attributes. + +- Announce changes in guild attributes. + - Application status + - Disband warning ## Example ![image](https://user-images.githubusercontent.com/12865379/29383497-7df48300-8285-11e7-83c3-f774ad3a43a8.png) diff --git a/config-example.json b/config-example.json deleted file mode 100644 index 8bbf0ed..0000000 --- a/config-example.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "__comment__": "To use this file, rename it to config.json", - "webhook_url": "http://discord.webhook.url.goes.here" - "guilds": [ - {"name": "Redd Alliance", "override_image": true}, - {"name": "Another guild"}, - {"name": "Yet another guild"}, - {"name": "Guild with announcements on diff channel", "webhook_url": "http://discord.webhook.url.goes.here"} - ] -} \ No newline at end of file diff --git a/config-example.yml b/config-example.yml new file mode 100644 index 0000000..e3e79c9 --- /dev/null +++ b/config-example.yml @@ -0,0 +1,15 @@ +# To use this file, rename it to config.yml + +webhook_url: http://discord.webhook.url.goes.here + +# Time in seconds to wait between checks. +interval: 300 + +# Remember to write the title with the correct casing. +guilds: + - Redd Alliance + - Bald Dwarfs + # If you want to override the channel, you must use this format for that entry + # The changes of this guild will be posted on a different channel. + - name: Academy + webhook_url: http://another.webhook.url.goes.here diff --git a/guildwatcher.py b/guildwatcher.py index 429685b..fc23a00 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -1,3 +1,25 @@ +""" +The MIT License (MIT) +Copyright (c) 2019 Allan Galarza + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" import json import logging import pickle @@ -6,6 +28,7 @@ import requests import tibiapy +import yaml log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -15,15 +38,17 @@ log.addHandler(consoleHandler) # Embed colors -CLR_NEW_MEMBER = 361051 # 05825B -CLR_REMOVED_MEMBER = 16711680 # FF0000 -CLR_PROMOTED = 16776960 # FFFF00 -CLR_DEMOTED = 16753920 # FFA500 -CLR_DELETED = 0 # 000000 -CLR_NAME_CHANGE = 65535 # 00FFFF -CLR_TITLE_CHANGE = 12915437 # C512ED -CLR_INVITE_REMOVED = 16738662 # FF6966 -CLR_NEW_INVITE = 8254857 # 7DF589 +CLR_NEW_MEMBER = 0x05825B # Dark green +CLR_REMOVED_MEMBER = 0xFF0000 # Red +CLR_PROMOTED = 0xFFFF00 # Yellow +CLR_DEMOTED = 0xFFA500 # Orange +CLR_DELETED = 0x000000 # Black +CLR_NAME_CHANGE = 0x00FFFF # Cyan +CLR_TITLE_CHANGE = 0xC512ED # Magenta +CLR_INVITE_REMOVED = 0xFF6966 # Light red +CLR_NEW_INVITE = 0x7DF589 # Lime green +CLR_GUILDHALL_REMOVE = 0xA9A9A9 # Grey +CLR_GUILDHALL_CHANGED = 0xFFFFFF # White # Change strings # m -> Member related to the change @@ -35,6 +60,8 @@ FMT_NAME_CHANGE = "{extra} → [{m.name}]({m.url}) - **{m.level}** **{v}** {e}\n" FMT_TITLE_CHANGE = "[{m.name}]({m.url}) - {extra} → {m.title} - **{m.level}** **{v}** {e}\n" FMT_INVITE_CHANGE = "[{m.name}]({m.url}) - Invited: **{m.date}**\n" +FMT_GUILDHALL_CHANGED = "Guild moved to guildhall **{extra}**" +FMT_GUILDHALL_REMOVE = "Guild no longer owns guildhall **{extra}**" class Change: @@ -45,15 +72,17 @@ class Change: :ivar type: The change type. :ivar extra: Extra information related to the change. :type member: abc.Character - :type type: str + :type type: ChangeType :type extra: Optional[Any] """ - def __init__(self, _type, member, extra=None): self.member = member self.type = _type self.extra = extra + def __repr__(self): + return "<%s type=%s member=%r>" % (self.__class__.__name__, self.type.name, self.member) + class ChangeType(Enum): """Contains all the possible changes that can be found.""" @@ -66,22 +95,45 @@ class ChangeType(Enum): PROMOTED = 7 #: Member was promoted. INVITE_REMOVED = 8 #: Invitation was removed or rejected. NEW_INVITE = 9 #: New invited + GUILDHALL_CHANGED = 10 #: Guild moved to a new guildhall. + GUILDHALL_REMOVED = 11 #: Guild no longer has a guildhall. + + +class ConfigGuild: + def __init__(self, name, webhook_url): + self.name = name + self.webhook_url = webhook_url -cfg = {} + def __repr__(self): + return "<%s name=%r webhook_url=%r>" % (self.__class__.__name__, self.name, self.webhook_url) +class Config: + def __init__(self, **kwargs): + guilds = kwargs.get("guilds", []) + self.webhook_url = kwargs.get("webhook_url") + self.interval = int(kwargs.get("interval", 300)) + self.guilds = [] + for guild in guilds: + if isinstance(guild, str): + self.guilds.append(ConfigGuild(guild, self.webhook_url)) + if isinstance(guild, dict): + self.guilds.append(ConfigGuild(guild["name"], guild["webhook_url"])) + + def __repr__(self): + return "<%s webhook_url=%r guilds=%r>" % (self.__class__.__name__, self.webhook_url, self.guilds) + def load_config(): """Loads and validates the configuration file.""" - global cfg try: - with open('config.json') as json_data: - cfg = json.load(json_data) + with open('config.yml') as yml_file: + cgf_yml = yaml.safe_load(yml_file) + return Config(**cgf_yml) except FileNotFoundError: - log.error("Missing config.json file. Check the example file.") - exit() - except ValueError: - log.error("Malformed config.json file.") - exit() + log.error("Missing config.yml file. Check the example file.") + except (ValueError, KeyError, TypeError) as e: + log.error("Malformed config.yml file.\nError: %s" % e) + exit() def save_data(file, data): @@ -188,7 +240,7 @@ def split_message(message): # pragma: no cover return message_list -def compare_guilds(before, after): +def compare_guild(before, after): """ Compares the same guild at different points in time, to obtain the changes made. @@ -202,10 +254,35 @@ def compare_guilds(before, after): :rtype: list of Change """ changes = [] - ranks = after.ranks[:] # Members no longer in guild. Some may have changed name. removed_members = [m for m in before.members if m not in after.members] joined = [m for m in after.members if m not in before.members] + + if before.guildhall != after.guildhall: + if before.guildhall is None: + changes.append(Change(ChangeType.GUILDHALL_CHANGED, None, after.guildhall.name)) + log.info("New guildhall: %s" % after.guildhall.name) + elif after.guildhall is None: + log.info("Guildhall removed: %s" % before.guildhall.name) + changes.append(Change(ChangeType.GUILDHALL_REMOVED, None, before.guildhall.name)) + + compare_members(after, before, changes) + check_removed_members(changes, joined, removed_members) + + changes += [Change(ChangeType.NEW_MEMBER, m) for m in joined] + if len(joined) > 0: + log.info("New members found: " + ",".join(m.name for m in joined)) + + compare_guild_invites(after, before, changes, joined) + return changes + + +def compare_members(after, before, changes): + """Compares the members still in the guild to see what changed. + + It compares the member's current state, with the previous member's state.""" + # ranks is property, so we save a copy to avoid recalculating it every time. + ranks = after.ranks[:] for member in before.members: for member_after in after.members: if member != member_after: @@ -228,6 +305,10 @@ def compare_guilds(before, after): log.info("Member title changed from '%s' to '%s'" % (member.title, member_after.title)) changes.append(Change(ChangeType.TITLE_CHANGE, member_after, member.title)) break + + +def check_removed_members(changes, joined, removed_members): + """Checks every removed member to see if they left, changed name or were deleted.""" for member in removed_members: # We check if it was a namechange or character deleted log.info("Checking character {0.name}".format(member)) @@ -249,10 +330,10 @@ def compare_guilds(before, after): if not found: log.info("Member no longer in guild: " + member.name) changes.append(Change(ChangeType.REMOVED, member)) - changes += [Change(ChangeType.NEW_MEMBER, m) for m in joined] - if len(joined) > 0: - log.info("New members found: " + ",".join(m.name for m in joined)) + +def compare_guild_invites(after, before, changes, joined): + """Compares invites, to see if they were accepted or rejected.""" new_invites = [i for i in after.invites if i not in before.invites] removed_invites = [i for i in before.invites if i not in after.invites] # Check if invitation got removed or member joined @@ -268,26 +349,25 @@ def compare_guilds(before, after): changes += [Change(ChangeType.NEW_INVITE, i) for i in new_invites] if len(new_invites) > 0: log.info("New invites found: " + ",".join(m.name for m in new_invites)) - return changes def get_vocation_emoji(vocation): """Returns an emoji to represent a character's vocation. :param vocation: The vocation's name. - :type vocation: str + :type vocation: tibiapy.Vocation :return: The emoji that represents the vocation. :rtype: str """ return { - "Druid": "\U00002744", - "Elder Druid": "\U00002744", - "Knight": "\U0001F6E1", - "Elite Knight": "\U0001F6E1", - "Sorcerer": "\U0001F525", - "Master Sorcerer": "\U0001F525", - "Paladin": "\U0001F3F9", - "Royal Paladin": "\U0001F3F9", + tibiapy.Vocation.DRUID: "\U00002744", + tibiapy.Vocation.ELDER_DRUID: "\U00002744", + tibiapy.Vocation.KNIGHT: "\U0001F6E1", + tibiapy.Vocation.ELITE_KNIGHT: "\U0001F6E1", + tibiapy.Vocation.SORCERER: "\U0001F525", + tibiapy.Vocation.MASTER_SORCERER: "\U0001F525", + tibiapy.Vocation.PALADIN: "\U0001F3F9", + tibiapy.Vocation.ROYAL_PALADIN: "\U0001F3F9", }.get(vocation, "") @@ -295,19 +375,19 @@ def get_vocation_abbreviation(vocation): """Gets an abbreviated string of the vocation. :param vocation: The vocation's name - :type vocation: str + :type vocation: tibiapy.Vocation :return: The emoji that represents the vocation. :rtype: str""" return { - "Druid": "D", - "Elder Druid": "ED", - "Knight": "K", - "Elite Knight": "EK", - "Sorcerer": "S", - "Master Sorcerer": "MS", - "Paladin": "P", - "Royal Paladin": "RP", - "None": "N", + tibiapy.Vocation.DRUID: "D", + tibiapy.Vocation.ELDER_DRUID: "ED", + tibiapy.Vocation.KNIGHT: "K", + tibiapy.Vocation.ELITE_KNIGHT: "EK", + tibiapy.Vocation.SORCERER: "S", + tibiapy.Vocation.MASTER_SORCERER: "MS", + tibiapy.Vocation.PALADIN: "P", + tibiapy.Vocation.ROYAL_PALADIN: "RP", + tibiapy.Vocation.NONE: "N", }.get(vocation, "") @@ -356,6 +436,12 @@ def build_embeds(changes): new_invites += FMT_INVITE_CHANGE.format(m=change.member) elif change.type == ChangeType.INVITE_REMOVED: removed_invites += FMT_INVITE_CHANGE.format(m=change.member) + elif change.type == ChangeType.GUILDHALL_REMOVED: + embeds.append({"color": CLR_GUILDHALL_REMOVE, "title": "Guildhall removed", + "description": FMT_GUILDHALL_REMOVE.format(extra=change.extra)}) + elif change.type == ChangeType.GUILDHALL_CHANGED: + embeds.append({"color": CLR_GUILDHALL_CHANGED, "title": "Guildhall changed", + "description": FMT_GUILDHALL_CHANGED.format(extra=change.extra)}) if new_members: messages = split_message(new_members) @@ -442,14 +528,14 @@ def publish_changes(url, embeds, name=None, avatar=None, new_count=0): def scan_guilds(): - load_config() + cfg = load_config() + if not cfg.webhook_url: + log.error("Missing Webhook URL in config.yml") + exit() while True: # Iterate through each guild in the configuration file - for cfg_guild in cfg["guilds"]: - if cfg_guild.get("webhook_url", cfg.get("webhook_url")) is None: - log.error("Missing Webhook URL in config.json") - exit() - name = cfg_guild.get("name") + for cfg_guild in cfg.guilds: + name = cfg_guild.name if name is None: log.error("Guild is missing name.") time.sleep(5) @@ -478,16 +564,13 @@ def scan_guilds(): # Only publish count if it changed if member_count == member_count_before: member_count = 0 - changes = compare_guilds(guild_data, new_guild_data) - if cfg_guild["override_image"]: - cfg_guild["avatar_url"] = new_guild_data.logo_url + changes = compare_guild(guild_data, new_guild_data) embeds = build_embeds(changes) - publish_changes(cfg_guild.get("webhook_url", cfg.get("webhook_url")), embeds, guild_data.name, - cfg_guild["avatar_url"], member_count) + publish_changes(cfg_guild.webhook_url, embeds, guild_data.name, new_guild_data.logo_url, member_count) log.info(name + " - Scanning done") time.sleep(2) - time.sleep(5 * 60) + time.sleep(cfg.interval) if __name__ == "__main__": - scan_guilds() \ No newline at end of file + scan_guilds() diff --git a/requirements.txt b/requirements.txt index 7cc2cb6..7b917f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests -tibia.py==0.* \ No newline at end of file +tibia.py +PyYAML \ No newline at end of file diff --git a/setup.py b/setup.py index 9ca5c1d..ae372c7 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='guildwatcher', - version='0.2.0', + version='1.0.0', author='Allan Galarza', author_email="allan.galarza@gmail.com", description='A discord webhook to track Tibia guild changes.', diff --git a/test_guildwatcher.py b/test_guildwatcher.py index a6a98f8..fed272e 100644 --- a/test_guildwatcher.py +++ b/test_guildwatcher.py @@ -1,100 +1,172 @@ import copy import datetime -import json +import logging import unittest from datetime import date -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch, mock_open import requests -from tibiapy import Guild, GuildMember, Character, GuildInvite +from tibiapy import Guild, GuildMember, Character, GuildInvite, Vocation, GuildHouse import guildwatcher from guildwatcher import Change, ChangeType +logger = logging.getLogger(guildwatcher.__name__) class TestGuildWatcher(unittest.TestCase): def setUp(self): self.guild = Guild("Test Guild", "Antica") + self.guild.guildhall = GuildHouse("Crystal Glance", self.guild.world) today = datetime.date.today() self.guild.members = [ - GuildMember("Galarzaa", "Leader", level=285, vocation="Royal Paladin", joined=today), - GuildMember("Nezune", "Vice", level=412, vocation="Elite Knight", title="Nab", joined=today), - GuildMember("Ondskan", "Vice", level=437, vocation="Royal Paladin", joined=today), - GuildMember("Faenryz", "Vice", level=207, vocation="Royal Paladin", joined=today), - GuildMember("Tschis", "Elite", level=205, vocation="Druid", joined=today), - GuildMember("John Doe", "Elite", level=34, vocation="Master Sorcerer", joined=today), - GuildMember("Jane Doe", "Recruit", level=55, vocation="Sorcerer", joined=today), - GuildMember("Fahgnoli", "Recruit", level=404, vocation="Master Sorcerer", joined=today) + GuildMember("Galarzaa", "Leader", level=285, vocation=Vocation.ROYAL_PALADIN, joined=today), + GuildMember("Nezune", "Vice", level=412, vocation=Vocation.ELITE_KNIGHT, title="Nab", joined=today), + GuildMember("Ondskan", "Vice", level=437, vocation=Vocation.ROYAL_PALADIN, joined=today), + GuildMember("Faenryz", "Vice", level=207, vocation=Vocation.ROYAL_PALADIN, joined=today), + GuildMember("Tschis", "Elite", level=205, vocation=Vocation.DRUID, joined=today), + GuildMember("John Doe", "Elite", level=34, vocation=Vocation.MASTER_SORCERER, joined=today), + GuildMember("Jane Doe", "Recruit", level=55, vocation=Vocation.SORCERER, joined=today), + GuildMember("Fahgnoli", "Recruit", level=404, vocation=Vocation.MASTER_SORCERER, joined=today) ] self.guild.invites = [ GuildInvite("Xzilla") ] self.guild_after = copy.deepcopy(self.guild) - def testPromotedMember(self): + @patch('logging.Logger.error') + @patch('builtins.open', new_callable=mock_open, read_data='') + def test_config_empty(self, m_open, log_error): + """Attempt loading an empty config file""" + with self.assertRaises(SystemExit): + guildwatcher.load_config() + log_error.assert_called_once() + + @patch('logging.Logger.error') + @patch('builtins.open', new_callable=mock_open) + def test_config_no_file(self, m_open, log_error): + """Attempt loading a file that doesn't exist.""" + m_open.side_effect = FileNotFoundError() + with self.assertRaises(SystemExit): + guildwatcher.load_config() + + def test_config_simple_file(self): + """Testing a config file with simple guild syntax.""" + webhook_url = "http://discord.webhook.url.goes.here" + content = """ + webhook_url: %s + + guilds: + - Redd Alliance + """ % webhook_url + with patch('builtins.open', new_callable=mock_open, read_data=content): + cfg = guildwatcher.load_config() + + self.assertIsInstance(cfg, guildwatcher.Config) + self.assertEqual(webhook_url, cfg.webhook_url) + self.assertEqual(1, len(cfg.guilds)) + self.assertEqual("Redd Alliance", cfg.guilds[0].name) + self.assertEqual(webhook_url, cfg.guilds[0].webhook_url) + + def test_config_advanced_file(self): + """Testing a config file with advanced guild syntax.""" + webhook_url = "http://discord.webhook.url.goes.here" + second_webhook = "http://another.webhook.url" + content = """ + webhook_url: %s + + guilds: + - Redd Alliance + - name: Bald Dwarfs + webhook_url: %s + """ % (webhook_url, second_webhook) + with patch('builtins.open', new_callable=mock_open, read_data=content): + cfg = guildwatcher.load_config() + + self.assertIsInstance(cfg, guildwatcher.Config) + self.assertEqual(webhook_url, cfg.webhook_url) + self.assertEqual(2, len(cfg.guilds)) + self.assertEqual("Redd Alliance", cfg.guilds[0].name) + self.assertEqual(webhook_url, cfg.guilds[0].webhook_url) + + self.assertEqual("Bald Dwarfs", cfg.guilds[1].name) + self.assertEqual(second_webhook, cfg.guilds[1].webhook_url) + + def test_new_guildhall(self): + self.guild.guildhall = None + changes = guildwatcher.compare_guild(self.guild, self.guild_after) + self.assertEqual(changes[0].type, guildwatcher.ChangeType.GUILDHALL_CHANGED) + self.assertEqual(changes[0].extra, self.guild_after.guildhall.name) + + def test_lost_guildhall(self): + self.guild_after.guildhall = None + changes = guildwatcher.compare_guild(self.guild, self.guild_after) + self.assertEqual(changes[0].type, guildwatcher.ChangeType.GUILDHALL_REMOVED) + self.assertEqual(changes[0].extra, self.guild.guildhall.name) + + def test_promoted_member(self): new_rank = "Elite" promoted_member = self.guild_after.members[6] promoted_member.rank = new_rank - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.PROMOTED) self.assertEqual(changes[0].member.name, promoted_member.name) self.assertEqual(changes[0].member.rank, promoted_member.rank) - def testDemotedMember(self): + def test_demoted_member(self): new_rank = "Recruit" demoted_member = self.guild_after.members[5] demoted_member.rank = new_rank - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.DEMOTED) self.assertEqual(changes[0].member.name, demoted_member.name) self.assertEqual(changes[0].member.rank, demoted_member.rank) - def testNewMember(self): + def test_new_member(self): new_member = GuildMember("Noob", "Recruit", level=12, vocation="Knight") self.guild_after.members.append(new_member) - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.NEW_MEMBER) self.assertEqual(changes[0].member.name, new_member.name) - def testTitleChange(self): + def test_title_change(self): new_title = "Even Nabber" affected_member = self.guild_after.members[1] old_title = affected_member.title affected_member.title = new_title - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.TITLE_CHANGE) self.assertEqual(changes[0].member.name, affected_member.name) self.assertEqual(changes[0].member.title, new_title) self.assertEqual(changes[0].extra, old_title) - def testMemberDeleted(self): + def test_member_deleted(self): # Kick member at position 6 kicked = self.guild_after.members.pop(6) # Mock get_character to imitate non existing character guildwatcher.get_character = MagicMock(return_value=None) - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.DELETED) self.assertEqual(changes[0].member.name, kicked.name) guildwatcher.get_character.assert_called_with(kicked.name) - def testMemberKicked(self): + def test_member_kicked(self): # Kick member at position 1 kicked = self.guild_after.members.pop(1) # Mock get_character to imitate existing character guildwatcher.get_character = MagicMock(return_value=Character(name=kicked.name)) - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.REMOVED) self.assertEqual(changes[0].member.name, kicked.name) guildwatcher.get_character.assert_called_with(kicked.name) - def testMemberNameChanged(self): + def test_member_name_changed(self): # Change name of first member new_name = "Galarzaa Fidera" affected_member = self.guild_after.members[0] @@ -104,61 +176,64 @@ def testMemberNameChanged(self): # Checking the missing character should return the new name guildwatcher.get_character = MagicMock(return_value=Character(name=new_name)) - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.NAME_CHANGE) self.assertEqual(changes[0].member.name, new_name) self.assertEqual(changes[0].extra, old_name) guildwatcher.get_character.assert_called_with(old_name) - def testInviteAccepted(self): + def test_invite_accepted(self): joining_member = self.guild_after.invites.pop() - self.guild_after.members.append(GuildMember(joining_member.name, "Recruit", None, 400, "Master Sorcerer")) + self.guild_after.members.append(GuildMember(joining_member.name, "Recruit", None, 400, Vocation.MASTER_SORCERER)) - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.NEW_MEMBER) self.assertEqual(changes[0].member.name, joining_member.name) - def testInviteRemoved(self): + def test_invite_removed(self): joining_member = self.guild_after.invites.pop() - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.INVITE_REMOVED) self.assertEqual(changes[0].member.name, joining_member.name) - def testNewInvite(self): + def test_new_invite(self): new_invite = GuildInvite("Pecorino") self.guild_after.invites.append(new_invite) - changes = guildwatcher.compare_guilds(self.guild, self.guild_after) + changes = guildwatcher.compare_guild(self.guild, self.guild_after) self.assertEqual(changes[0].type, guildwatcher.ChangeType.NEW_INVITE) self.assertEqual(changes[0].member.name, new_invite.name) - def testDataIntegrity(self): + def test_data_integrity(self): guildwatcher.save_data(".tmp.data", self.guild) saved_guild = guildwatcher.load_data(".tmp.data") - changes = guildwatcher.compare_guilds(self.guild, saved_guild) + changes = guildwatcher.compare_guild(self.guild, saved_guild) self.assertFalse(changes) - def testEmbeds(self): + def test_embeds(self): changes = [ - Change(ChangeType.NEW_MEMBER, GuildMember("Noob", "Recruit", level=19, vocation="Druid")), - Change(ChangeType.REMOVED, GuildMember("John", "Member", level=56, vocation="Druid", joined=date.today())), - Change(ChangeType.NAME_CHANGE, GuildMember("Tschis", "Vice", level=205, vocation="Druid"), "Tschas"), - Change(ChangeType.DELETED, GuildMember("Botter", "Vice", level=444, vocation="Elite Knight", + Change(ChangeType.NEW_MEMBER, GuildMember("Noob", "Recruit", level=19, vocation=Vocation.DRUID)), + Change(ChangeType.REMOVED, GuildMember("John", "Member", level=56, vocation=Vocation.DRUID, joined=date.today())), + Change(ChangeType.NAME_CHANGE, GuildMember("Tschis", "Vice", level=205, vocation=Vocation.DRUID), "Tschas"), + Change(ChangeType.DELETED, GuildMember("Botter", "Vice", level=444, vocation=Vocation.ELITE_KNIGHT, joined=date.today())), - Change(ChangeType.TITLE_CHANGE, GuildMember("Nezune", level=404, rank="Vice", vocation="Elite Knight", + Change(ChangeType.TITLE_CHANGE, GuildMember("Nezune", level=404, rank="Vice", vocation=Vocation.ELITE_KNIGHT, title="Nab"), "Challenge Pls"), - Change(ChangeType.PROMOTED, GuildMember("Old", "Rank", level=142, vocation="Royal Paladin", + Change(ChangeType.PROMOTED, GuildMember("Old", "Rank", level=142, vocation=Vocation.ROYAL_PALADIN, joined=date.today())), - Change(ChangeType.DEMOTED, GuildMember("Jane", "Rank", level=89, vocation="Master Sorcerer", + Change(ChangeType.DEMOTED, GuildMember("Jane", "Rank", level=89, vocation=Vocation.MASTER_SORCERER, joined=date.today())), Change(ChangeType.INVITE_REMOVED, GuildInvite("Unwanted", date=date.today())), - Change(ChangeType.NEW_INVITE, GuildInvite("Good Guy", date=date.today())) + Change(ChangeType.NEW_INVITE, GuildInvite("Good Guy", date=date.today())), + Change(ChangeType.GUILDHALL_REMOVED, None, "Crystal Glance"), + Change(ChangeType.GUILDHALL_CHANGED, None, "The Tibianic"), ] embeds = guildwatcher.build_embeds(changes) - print(json.dumps(embeds, indent=2)) + import pprint + pprint.pprint(embeds) self.assertTrue(embeds) requests.post = MagicMock() guildwatcher.publish_changes("https://canary.discordapp.com/api/webhooks/webhook", embeds)