From e805f0911ad17a1a57ee4a047c5547cf733cd387 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Tue, 28 Aug 2018 17:28:00 -0700 Subject: [PATCH 01/16] Corrected example config file It was missing a comma, so the config file was invalid. --- config-example.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-example.json b/config-example.json index 8bbf0ed..171c71a 100644 --- a/config-example.json +++ b/config-example.json @@ -1,10 +1,10 @@ { "__comment__": "To use this file, rename it to config.json", - "webhook_url": "http://discord.webhook.url.goes.here" + "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 +} From 7f0a4914a4d467428cb18115e9c3abb3112eb62c Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Sun, 30 Jun 2019 08:51:20 -0700 Subject: [PATCH 02/16] Updated dependencies --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7cc2cb6..f92662c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests -tibia.py==0.* \ No newline at end of file +tibia.py +discord.py \ No newline at end of file From 47fc9f79ed487a7bcc56f497c97ef7d199783801 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 1 Jul 2019 10:05:40 -0700 Subject: [PATCH 03/16] Fixed vocation display --- guildwatcher.py | 20 ++++++++-------- test_guildwatcher.py | 56 ++++++++++++++++++++++---------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/guildwatcher.py b/guildwatcher.py index 429685b..f8a4c72 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -188,7 +188,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. @@ -280,14 +280,14 @@ def get_vocation_emoji(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, "") @@ -478,7 +478,7 @@ 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) + changes = compare_guild(guild_data, new_guild_data) if cfg_guild["override_image"]: cfg_guild["avatar_url"] = new_guild_data.logo_url embeds = build_embeds(changes) diff --git a/test_guildwatcher.py b/test_guildwatcher.py index a6a98f8..d2615b2 100644 --- a/test_guildwatcher.py +++ b/test_guildwatcher.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import requests -from tibiapy import Guild, GuildMember, Character, GuildInvite +from tibiapy import Guild, GuildMember, Character, GuildInvite, Vocation import guildwatcher from guildwatcher import Change, ChangeType @@ -17,14 +17,14 @@ def setUp(self): self.guild = Guild("Test Guild", "Antica") 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") @@ -36,7 +36,7 @@ def testPromotedMember(self): 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) @@ -46,7 +46,7 @@ def testDemotedMember(self): 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) @@ -55,7 +55,7 @@ def testNewMember(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) @@ -64,7 +64,7 @@ def testTitleChange(self): 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) @@ -77,7 +77,7 @@ def testMemberDeleted(self): # 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) @@ -89,7 +89,7 @@ def testMemberKicked(self): # 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) @@ -104,7 +104,7 @@ 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) @@ -112,16 +112,16 @@ def testMemberNameChanged(self): def testInviteAccepted(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): 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) @@ -129,7 +129,7 @@ def testNewInvite(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) @@ -137,22 +137,22 @@ def testDataIntegrity(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): 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())) From 4aad8230064de7b98bec5ac0ca94339dcbdbfdcc Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 1 Jul 2019 14:48:47 -0700 Subject: [PATCH 04/16] Migrated config file to YAML --- .gitignore | 1 + config-example.yml | 12 +++++++++ guildwatcher.py | 67 ++++++++++++++++++++++++++++------------------ requirements.txt | 3 +-- 4 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 config-example.yml 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/config-example.yml b/config-example.yml new file mode 100644 index 0000000..0236ebc --- /dev/null +++ b/config-example.yml @@ -0,0 +1,12 @@ +# To use this file, rename it to config.yml + +webhook_url: http://discord.webhook.url.goes.here + +# 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 f8a4c72..564ac02 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -6,6 +6,7 @@ import requests import tibiapy +import yaml log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -15,15 +16,15 @@ 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 +CLR_REMOVED_MEMBER = 0xFF0000 +CLR_PROMOTED = 0xFFFF00 +CLR_DEMOTED = 0xFFA500 +CLR_DELETED = 0x000000 +CLR_NAME_CHANGE = 0x00FFFF +CLR_TITLE_CHANGE = 0xC512ED +CLR_INVITE_REMOVED = 0xFF6966 +CLR_NEW_INVITE = 0x7DF589 # Change strings # m -> Member related to the change @@ -67,20 +68,37 @@ class ChangeType(Enum): INVITE_REMOVED = 8 #: Invitation was removed or rejected. NEW_INVITE = 9 #: New invited -cfg = {} +class ConfigGuild: + def __init__(self, name, webhook_url): + self.name = name + self.webhook_url = webhook_url + + +class Config: + def __init__(self, **kwargs): + guilds = kwargs.get("guilds", []) + self.webhook_url = kwargs.get("webhook_url") + 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 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) + if cgf_yml is None: + cgf_yml = {} + return Config(**cgf_yml) except FileNotFoundError: - log.error("Missing config.json file. Check the example file.") + log.error("Missing config.yml file. Check the example file.") exit() - except ValueError: - log.error("Malformed config.json file.") + except (ValueError, KeyError) as e: + log.error("Malformed config.yml file.\nError: %s" % e) exit() @@ -442,14 +460,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) @@ -479,11 +497,8 @@ def scan_guilds(): if member_count == member_count_before: member_count = 0 changes = compare_guild(guild_data, new_guild_data) - if cfg_guild["override_image"]: - cfg_guild["avatar_url"] = new_guild_data.logo_url 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) diff --git a/requirements.txt b/requirements.txt index f92662c..a913434 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ requests -tibia.py -discord.py \ No newline at end of file +tibia.py \ No newline at end of file From da03dae52eb9afc93215513b773cee77c5f46761 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 1 Jul 2019 14:56:46 -0700 Subject: [PATCH 05/16] PEP8 compliant test cases --- test_guildwatcher.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test_guildwatcher.py b/test_guildwatcher.py index d2615b2..d6faf0b 100644 --- a/test_guildwatcher.py +++ b/test_guildwatcher.py @@ -31,7 +31,7 @@ def setUp(self): ] self.guild_after = copy.deepcopy(self.guild) - def testPromotedMember(self): + def test_promoted_member(self): new_rank = "Elite" promoted_member = self.guild_after.members[6] promoted_member.rank = new_rank @@ -41,7 +41,7 @@ def testPromotedMember(self): 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 @@ -51,7 +51,7 @@ def testDemotedMember(self): 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) @@ -59,7 +59,7 @@ def testNewMember(self): 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 @@ -70,7 +70,7 @@ def testTitleChange(self): 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) @@ -82,7 +82,7 @@ def testMemberDeleted(self): 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) @@ -94,7 +94,7 @@ def testMemberKicked(self): 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] @@ -110,7 +110,7 @@ def testMemberNameChanged(self): 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, Vocation.MASTER_SORCERER)) @@ -118,14 +118,14 @@ def testInviteAccepted(self): 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_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) @@ -133,7 +133,7 @@ def testNewInvite(self): 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") @@ -141,7 +141,7 @@ def testDataIntegrity(self): self.assertFalse(changes) - def testEmbeds(self): + def test_embeds(self): changes = [ 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())), From 289d40e459e3e4a0778f76996542448571969cb2 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 1 Jul 2019 15:01:07 -0700 Subject: [PATCH 06/16] Missing yaml in requirements --- requirements.txt | 3 ++- test_guildwatcher.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a913434..e9d3483 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests -tibia.py \ No newline at end of file +tibia.py +yaml \ No newline at end of file diff --git a/test_guildwatcher.py b/test_guildwatcher.py index d6faf0b..f49b5e5 100644 --- a/test_guildwatcher.py +++ b/test_guildwatcher.py @@ -1,6 +1,5 @@ import copy import datetime -import json import unittest from datetime import date from unittest.mock import MagicMock @@ -158,7 +157,6 @@ def test_embeds(self): Change(ChangeType.NEW_INVITE, GuildInvite("Good Guy", date=date.today())) ] embeds = guildwatcher.build_embeds(changes) - print(json.dumps(embeds, indent=2)) self.assertTrue(embeds) requests.post = MagicMock() guildwatcher.publish_changes("https://canary.discordapp.com/api/webhooks/webhook", embeds) From 6e63ecc3fa1ff44a872b1365b6143490b56a01b6 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 1 Jul 2019 15:04:01 -0700 Subject: [PATCH 07/16] Wrong yaml package --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9d3483..7b917f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests tibia.py -yaml \ No newline at end of file +PyYAML \ No newline at end of file From 3259488bfe00a7a9524db0502a0b8418cf497315 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 1 Jul 2019 17:06:05 -0700 Subject: [PATCH 08/16] Added config reading test case --- guildwatcher.py | 4 +--- test_guildwatcher.py | 7 ++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/guildwatcher.py b/guildwatcher.py index 564ac02..abf513e 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -91,13 +91,11 @@ def load_config(): try: with open('config.yml') as yml_file: cgf_yml = yaml.safe_load(yml_file) - if cgf_yml is None: - cgf_yml = {} return Config(**cgf_yml) except FileNotFoundError: log.error("Missing config.yml file. Check the example file.") exit() - except (ValueError, KeyError) as e: + except (ValueError, KeyError, TypeError) as e: log.error("Malformed config.yml file.\nError: %s" % e) exit() diff --git a/test_guildwatcher.py b/test_guildwatcher.py index f49b5e5..68e535d 100644 --- a/test_guildwatcher.py +++ b/test_guildwatcher.py @@ -2,7 +2,7 @@ import datetime 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, Vocation @@ -30,6 +30,11 @@ def setUp(self): ] self.guild_after = copy.deepcopy(self.guild) + @patch('builtins.open', new_callable=mock_open, read_data='') + def test_config_empty(self, m): + cfg = guildwatcher.load_config() + print(cfg) + def test_promoted_member(self): new_rank = "Elite" promoted_member = self.guild_after.members[6] From e706d2f58e09aa64f1795ac87f5b91267314e7b7 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 3 Jul 2019 11:11:06 -0700 Subject: [PATCH 09/16] Added config loading test cases --- guildwatcher.py | 9 +++++-- test_guildwatcher.py | 61 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/guildwatcher.py b/guildwatcher.py index abf513e..9ec1b23 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -74,6 +74,9 @@ def __init__(self, name, webhook_url): self.name = name self.webhook_url = webhook_url + def __repr__(self): + return "<%s name=%r webhook_url=%r>" % (self.__class__.__name__, self.name, self.webhook_url) + class Config: def __init__(self, **kwargs): @@ -86,6 +89,9 @@ def __init__(self, **kwargs): 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.""" try: @@ -94,10 +100,9 @@ def load_config(): return Config(**cgf_yml) except FileNotFoundError: log.error("Missing config.yml file. Check the example file.") - exit() except (ValueError, KeyError, TypeError) as e: log.error("Malformed config.yml file.\nError: %s" % e) - exit() + exit() def save_data(file, data): diff --git a/test_guildwatcher.py b/test_guildwatcher.py index 68e535d..40e29d6 100644 --- a/test_guildwatcher.py +++ b/test_guildwatcher.py @@ -1,5 +1,6 @@ import copy import datetime +import logging import unittest from datetime import date from unittest.mock import MagicMock, patch, mock_open @@ -10,6 +11,7 @@ import guildwatcher from guildwatcher import Change, ChangeType +logger = logging.getLogger(guildwatcher.__name__) class TestGuildWatcher(unittest.TestCase): def setUp(self): @@ -30,10 +32,63 @@ def setUp(self): ] self.guild_after = copy.deepcopy(self.guild) + @patch('logging.Logger.error') @patch('builtins.open', new_callable=mock_open, read_data='') - def test_config_empty(self, m): - cfg = guildwatcher.load_config() - print(cfg) + 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_promoted_member(self): new_rank = "Elite" From 22589a72b89a4feb0204d3e0e495419a10df91a6 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 3 Jul 2019 11:41:40 -0700 Subject: [PATCH 10/16] Fixed vocation abbreviation --- .coveragerc | 3 ++- guildwatcher.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) 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/guildwatcher.py b/guildwatcher.py index 9ec1b23..6f84d6b 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -320,15 +320,15 @@ def get_vocation_abbreviation(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, "") From ade5c2de95545d7c18a55bf0490db6c990fb2d28 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 3 Jul 2019 12:28:52 -0700 Subject: [PATCH 11/16] Extracted compare_guild into smaller functions --- guildwatcher.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/guildwatcher.py b/guildwatcher.py index 6f84d6b..838c2d9 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -46,15 +46,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.""" @@ -223,10 +225,27 @@ def compare_guild(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] + + 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: @@ -249,6 +268,10 @@ def compare_guild(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)) @@ -270,10 +293,9 @@ def compare_guild(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): 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 @@ -289,7 +311,6 @@ def compare_guild(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): From 381075dcc2c40edd9fe2807cc0e056cd1351fe48 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 3 Jul 2019 13:11:00 -0700 Subject: [PATCH 12/16] Announce guildhall changes --- guildwatcher.py | 40 ++++++++++++++++++++++++++++++---------- test_guildwatcher.py | 21 +++++++++++++++++++-- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/guildwatcher.py b/guildwatcher.py index 838c2d9..6b9e1a8 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -16,15 +16,17 @@ log.addHandler(consoleHandler) # Embed colors -CLR_NEW_MEMBER = 0x05825B -CLR_REMOVED_MEMBER = 0xFF0000 -CLR_PROMOTED = 0xFFFF00 -CLR_DEMOTED = 0xFFA500 -CLR_DELETED = 0x000000 -CLR_NAME_CHANGE = 0x00FFFF -CLR_TITLE_CHANGE = 0xC512ED -CLR_INVITE_REMOVED = 0xFF6966 -CLR_NEW_INVITE = 0x7DF589 +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 @@ -36,7 +38,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: """ @@ -69,6 +72,8 @@ 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: @@ -229,6 +234,14 @@ def compare_guild(before, after): 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) @@ -296,6 +309,7 @@ def check_removed_members(changes, joined, removed_members): 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 @@ -398,6 +412,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) diff --git a/test_guildwatcher.py b/test_guildwatcher.py index 40e29d6..fed272e 100644 --- a/test_guildwatcher.py +++ b/test_guildwatcher.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch, mock_open import requests -from tibiapy import Guild, GuildMember, Character, GuildInvite, Vocation +from tibiapy import Guild, GuildMember, Character, GuildInvite, Vocation, GuildHouse import guildwatcher from guildwatcher import Change, ChangeType @@ -16,6 +16,7 @@ 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=Vocation.ROYAL_PALADIN, joined=today), @@ -90,6 +91,18 @@ def test_config_advanced_file(self): 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] @@ -214,9 +227,13 @@ def test_embeds(self): 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) + import pprint + pprint.pprint(embeds) self.assertTrue(embeds) requests.post = MagicMock() guildwatcher.publish_changes("https://canary.discordapp.com/api/webhooks/webhook", embeds) From fe75203b0c093003a24f522296e055b586ec6554 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Fri, 5 Jul 2019 07:03:26 -0700 Subject: [PATCH 13/16] Updated dependencies --- CHANGELOG.md | 5 +++++ README.md | 12 +++++++----- config-example.json | 10 ---------- guildwatcher.py | 4 ++-- 4 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 config-example.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 57eb711..ad0ed5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +## Version 1.0.0 (Unreleased) +- Now using YAML instead of json for configuration. +- Now announces guildhall changes. +- 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..5746afa 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: @@ -60,6 +59,7 @@ python -m guildwatcher * 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. * Webhook URL configurable per guild. @@ -67,8 +67,10 @@ python -m guildwatcher * 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. +- Configurable scan times. +- 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 171c71a..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"} - ] -} diff --git a/guildwatcher.py b/guildwatcher.py index 6b9e1a8..c45cf49 100644 --- a/guildwatcher.py +++ b/guildwatcher.py @@ -331,7 +331,7 @@ 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 """ @@ -351,7 +351,7 @@ 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 { From cad87fc366c67464ce175f4e21db744ecac10b24 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 8 Jul 2019 08:04:49 -0700 Subject: [PATCH 14/16] Configurable scan interval --- CHANGELOG.md | 1 + README.md | 25 +++++++++++++------------ config-example.yml | 3 +++ guildwatcher.py | 28 ++++++++++++++++++++++++++-- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0ed5a..3b9da2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Version 1.0.0 (Unreleased) - 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) diff --git a/README.md b/README.md index 5746afa..ecbc1fa 100644 --- a/README.md +++ b/README.md @@ -52,22 +52,23 @@ 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. -* Announce when the guildhall changes. -* 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. - Application status - Disband warning diff --git a/config-example.yml b/config-example.yml index 0236ebc..e3e79c9 100644 --- a/config-example.yml +++ b/config-example.yml @@ -2,6 +2,9 @@ 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 diff --git a/guildwatcher.py b/guildwatcher.py index c45cf49..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 @@ -41,6 +63,7 @@ FMT_GUILDHALL_CHANGED = "Guild moved to guildhall **{extra}**" FMT_GUILDHALL_REMOVE = "Guild no longer owns guildhall **{extra}**" + class Change: """ Represents a change found in the guild. @@ -89,6 +112,7 @@ 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): @@ -545,8 +569,8 @@ def scan_guilds(): 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() From 5e2861cd9ba3d941aa541ef8ebfc979c60f7af6c Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 8 Jul 2019 08:09:34 -0700 Subject: [PATCH 15/16] Updated version name --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.', From 4f8f28448ffe85ca5a0694147090e06d8e21910e Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 8 Jul 2019 08:10:03 -0700 Subject: [PATCH 16/16] Updated release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9da2b..4a55cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## Version 1.0.0 (Unreleased) +## 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.