Skip to content

Commit 10a3409

Browse files
Merge branch 'master' into type-checking
2 parents f5ef6f6 + 17079ad commit 10a3409

11 files changed

+441
-82
lines changed

.github/reviewers.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
reviewers:
2+
# The default reviewers
3+
defaults:
4+
- team:DozerDevs
5+
- team:SeniorDevs
6+
7+
options:
8+
ignore_draft: true
9+
ignored_keywords:
10+
- DO NOT REVIEW
11+
enable_group_assignment: false
12+

.github/workflows/main.yml

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ jobs:
88
lint:
99
# The type of runner that the job will run on
1010
runs-on: ubuntu-latest
11+
if: github.event_name == 'push'
1112

1213
steps:
1314
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
@@ -23,3 +24,15 @@ jobs:
2324
# Runs a set of commands using the runners shell
2425
- name: Lint
2526
run: ci/ci.sh
27+
28+
auto-request-review:
29+
name: Auto Request Review
30+
runs-on: ubuntu-latest
31+
if: github.event_name == 'pull_request'
32+
steps:
33+
- name: Request review based on files changes and/or groups the author belongs to
34+
uses: necojackarc/auto-request-review@v0.7.0
35+
with:
36+
token: ${{ secrets.BOTUSER }}
37+
config: .github/reviewers.yml
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Holder for the custom join/leave messages database class and the associated methods"""
2+
from dozer import db
3+
import discord
4+
from logging import getLogger
5+
6+
DOZER_LOGGER = getLogger(__name__)
7+
8+
9+
async def send_log(member):
10+
"""Sends the message for when a user joins or leave a guild"""
11+
config = await CustomJoinLeaveMessages.get_by(guild_id=member.guild.id)
12+
if len(config):
13+
channel = member.guild.get_channel(config[0].channel_id)
14+
if channel:
15+
embed = discord.Embed(color=0x00FF00)
16+
embed.set_author(name='Member Joined', icon_url=member.avatar_url_as(format='png', size=32))
17+
embed.description = format_join_leave(config[0].join_message, member)
18+
embed.set_footer(text="{} | {} members".format(member.guild.name, member.guild.member_count))
19+
try:
20+
await channel.send(content=member.mention if config[0].ping else None, embed=embed)
21+
except discord.Forbidden:
22+
DOZER_LOGGER.warning(
23+
f"Guild {member.guild}({member.guild.id}) has invalid permissions for join/leave logs")
24+
25+
26+
def format_join_leave(template: str, member: discord.Member):
27+
"""Formats join leave message templates
28+
{guild} = guild name
29+
{user} = user's name plus discriminator ex. SnowPlow#5196
30+
{user_name} = user's name without discriminator
31+
{user_mention} = user's mention
32+
{user_id} = user's ID
33+
"""
34+
if template:
35+
return template.format(guild=member.guild, user=str(member), user_name=member.name,
36+
user_mention=member.mention, user_id=member.id)
37+
else:
38+
return "{user_mention}\n{user} ({user_id})".format(user=str(member), user_mention=member.mention,
39+
user_id=member.id)
40+
41+
42+
class CustomJoinLeaveMessages(db.DatabaseTable):
43+
"""Holds custom join leave messages"""
44+
__tablename__ = 'memberlogconfig'
45+
__uniques__ = 'guild_id'
46+
47+
@classmethod
48+
async def initial_create(cls):
49+
"""Create the table in the database"""
50+
async with db.Pool.acquire() as conn:
51+
await conn.execute(f"""
52+
CREATE TABLE {cls.__tablename__} (
53+
guild_id bigint PRIMARY KEY NOT NULL,
54+
memberlog_channel bigint NOT NULL,
55+
name varchar NOT NULL
56+
)""")
57+
58+
def __init__(self, guild_id, channel_id=None, ping=None, join_message=None, leave_message=None):
59+
super().__init__()
60+
self.guild_id = guild_id
61+
self.channel_id = channel_id
62+
self.ping = ping
63+
self.join_message = join_message
64+
self.leave_message = leave_message
65+
66+
@classmethod
67+
async def get_by(cls, **kwargs):
68+
results = await super().get_by(**kwargs)
69+
result_list = []
70+
for result in results:
71+
obj = CustomJoinLeaveMessages(guild_id=result.get("guild_id"), channel_id=result.get("channel_id"),
72+
ping=result.get("ping"),
73+
join_message=result.get("join_message"),
74+
leave_message=result.get("leave_message"))
75+
result_list.append(obj)
76+
return result_list
77+
78+
async def version_1(self):
79+
"""DB migration v1"""
80+
async with db.Pool.acquire() as conn:
81+
await conn.execute(f"""
82+
alter table memberlogconfig rename column memberlog_channel to channel_id;
83+
alter table memberlogconfig alter column channel_id drop not null;
84+
alter table {self.__tablename__} drop column IF EXISTS name;
85+
alter table {self.__tablename__}
86+
add IF NOT EXISTS ping boolean default False;
87+
alter table {self.__tablename__}
88+
add IF NOT EXISTS join_message text default null;
89+
alter table {self.__tablename__}
90+
add IF NOT EXISTS leave_message text default null;
91+
""")
92+
93+
__versions__ = [version_1]

dozer/Components/TeamNumbers.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from dozer import db
2+
3+
4+
class TeamNumbers(db.DatabaseTable):
5+
"""Database operations for tracking team associations."""
6+
__tablename__ = 'team_numbers'
7+
__uniques__ = 'user_id, team_number, team_type'
8+
9+
@classmethod
10+
async def initial_create(cls):
11+
"""Create the table in the database"""
12+
async with db.Pool.acquire() as conn:
13+
await conn.execute(f"""
14+
CREATE TABLE {cls.__tablename__} (
15+
user_id bigint NOT NULL,
16+
team_number bigint NOT NULL,
17+
team_type VARCHAR NOT NULL,
18+
PRIMARY KEY (user_id, team_number, team_type)
19+
)""")
20+
21+
def __init__(self, user_id, team_number, team_type):
22+
super().__init__()
23+
self.user_id = user_id
24+
self.team_number = team_number
25+
self.team_type = team_type
26+
27+
async def update_or_add(self):
28+
"""Assign the attribute to this object, then call this method to either insert the object if it doesn't exist in
29+
the DB or update it if it does exist. It will update every column not specified in __uniques__."""
30+
# This is its own functions because all columns must be unique, which breaks the syntax of the other one
31+
keys = []
32+
values = []
33+
for var, value in self.__dict__.items():
34+
# Done so that the two are guaranteed to be in the same order, which isn't true of keys() and values()
35+
if value is not None:
36+
keys.append(var)
37+
values.append(value)
38+
async with db.Pool.acquire() as conn:
39+
statement = f"""
40+
INSERT INTO {self.__tablename__} ({", ".join(keys)})
41+
VALUES({','.join(f'${i+1}' for i in range(len(values)))})
42+
"""
43+
await conn.execute(statement, *values)
44+
45+
@classmethod
46+
async def get_by(cls, **kwargs):
47+
results = await super().get_by(**kwargs)
48+
result_list = []
49+
for result in results:
50+
obj = TeamNumbers(user_id=result.get("user_id"),
51+
team_number=result.get("team_number"),
52+
team_type=result.get("team_type"))
53+
result_list.append(obj)
54+
return result_list
55+
56+
# noinspection SqlResolve
57+
@classmethod
58+
async def top10(cls, user_ids):
59+
"""Returns the top 10 team entries"""
60+
query = f"""SELECT team_type, team_number, count(*)
61+
FROM {cls.__tablename__}
62+
WHERE user_id = ANY($1) --first param: list of user IDs
63+
GROUP BY team_type, team_number
64+
ORDER BY count DESC, team_type, team_number
65+
LIMIT 10"""
66+
async with db.Pool.acquire() as conn:
67+
return await conn.fetch(query, user_ids)

dozer/cogs/actionlogs.py

+14-35
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import discord
99
from discord.ext import commands
1010
from discord.ext.commands import has_permissions, BadArgument
11-
1211
from dozer.context import DozerContext
12+
1313
from ._utils import *
1414
from .general import blurple
1515
from .. import db
@@ -46,38 +46,17 @@ async def check_audit(guild, event_type, event_time=None):
4646
except discord.Forbidden:
4747
return None
4848

49-
@staticmethod
50-
def format_join_leave(template: str, member: discord.Member):
51-
"""Formats join leave message templates
52-
{guild} = guild name
53-
{user} = user's name plus discriminator ex. SnowPlow#5196
54-
{user_name} = user's name without discriminator
55-
{user_mention} = user's mention
56-
{user_id} = user's ID
57-
"""
58-
if template:
59-
return template.format(guild=member.guild, user=str(member), user_name=member.name,
60-
user_mention=member.mention, user_id=member.id)
61-
else:
62-
return "{user_mention}\n{user} ({user_id})".format(user=str(member), user_mention=member.mention,
63-
user_id=member.id)
64-
6549
@Cog.listener('on_member_join')
6650
async def on_member_join(self, member):
6751
"""Logs that a member joined, with optional custom message"""
68-
config = await CustomJoinLeaveMessages.get_by(guild_id=member.guild.id)
69-
if len(config):
70-
channel = member.guild.get_channel(config[0].channel_id)
71-
if channel:
72-
embed = discord.Embed(color=0x00FF00)
73-
embed.set_author(name='Member Joined', icon_url=member.avatar_url_as(format='png', size=32))
74-
embed.description = self.format_join_leave(config[0].join_message, member)
75-
embed.set_footer(text="{} | {} members".format(member.guild.name, member.guild.member_count))
76-
try:
77-
await channel.send(content=member.mention if config[0].ping else None, embed=embed)
78-
except discord.Forbidden:
79-
DOZER_LOGGER.warning(
80-
f"Guild {member.guild}({member.guild.id}) has invalid permissions for join/leave logs")
52+
nm_config = await GuildNewMember.get_by(guild_id=member.guild.id)
53+
if len(nm_config) == 0:
54+
await send_log(member)
55+
else:
56+
if nm_config[0].require_team:
57+
return
58+
else:
59+
await send_log(member)
8160

8261
@Cog.listener('on_member_remove')
8362
async def on_member_remove(self, member):
@@ -88,7 +67,7 @@ async def on_member_remove(self, member):
8867
if channel:
8968
embed = discord.Embed(color=0xFF0000)
9069
embed.set_author(name='Member Left', icon_url=member.avatar_url_as(format='png', size=32))
91-
embed.description = self.format_join_leave(config[0].leave_message, member)
70+
embed.description = format_join_leave(config[0].leave_message, member)
9271
embed.set_footer(text="{} | {} members".format(member.guild.name, member.guild.member_count))
9372
try:
9473
await channel.send(embed=embed)
@@ -337,6 +316,8 @@ async def on_message_edit(self, before: discord.Message, after: discord.Message)
337316
"""Logs message edits."""
338317
if before.author.bot:
339318
return
319+
if isinstance(before.channel, discord.DMChannel):
320+
return
340321
if after.edited_at is not None or before.edited_at is not None:
341322
# There is a reason for this. That reason is that otherwise, an infinite spam loop occurs
342323
guild_id = before.guild.id
@@ -429,9 +410,9 @@ async def memberlogconfig(self, ctx: DozerContext):
429410
embed.add_field(name="Message Channel", value=channel.mention if channel else "None")
430411
embed.add_field(name="Ping on join", value=config[0].ping)
431412
embed.add_field(name="Join template", value=config[0].join_message, inline=False)
432-
embed.add_field(name="Join Example", value=self.format_join_leave(config[0].join_message, ctx.author))
413+
embed.add_field(name="Join Example", value=format_join_leave(config[0].join_message, ctx.author))
433414
embed.add_field(name="Leave template", value=config[0].leave_message, inline=False)
434-
embed.add_field(name="Leave Example", value=self.format_join_leave(config[0].leave_message, ctx.author))
415+
embed.add_field(name="Leave Example", value=format_join_leave(config[0].leave_message, ctx.author))
435416
await ctx.send(embed=embed)
436417
else:
437418
await ctx.send("This guild has no member log configured")
@@ -625,7 +606,6 @@ async def get_by(cls, **kwargs):
625606
result_list.append(obj)
626607
return result_list
627608

628-
629609
class CustomJoinLeaveMessages(db.DatabaseTable):
630610
"""Holds custom join leave messages"""
631611
__tablename__ = 'memberlogconfig'
@@ -680,7 +660,6 @@ async def version_1(self):
680660

681661
__versions__ = [version_1]
682662

683-
684663
class GuildMessageLog(db.DatabaseTable):
685664
"""Holds config info for message logs"""
686665
__tablename__ = 'messagelogconfig'

dozer/cogs/info.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Provides commands for pulling certain information."""
22
import math
33
import typing
4-
from datetime import timezone, datetime
4+
from datetime import timezone, datetime, date
55
from difflib import SequenceMatcher
66

77
import discord
@@ -52,20 +52,22 @@ async def member(self, ctx: DozerContext, *, member: discord.Member = None):
5252
embed = discord.Embed(title=member.display_name, description=f'{member!s} ({member.id})', color=member.color)
5353
embed.set_thumbnail(url=member.avatar_url)
5454
embed.add_field(name='Bot Created' if member.bot else 'Account Created',
55-
value=member.created_at.strftime(datetime_format), inline=True)
55+
value=f"<t:{int(member.created_at.timestamp())}:f>", inline=True)
5656

5757
if not levels_enabled:
5858
embed.add_field(name="Last Seen Here At", value="Levels Disabled")
5959
elif len(levels_data):
60-
embed.add_field(name="Last Seen Here At", value=levels_data[0].last_given_at.strftime(datetime_format))
60+
61+
#
62+
embed.add_field(name="Last Seen Here", value=f"<t:{int(levels_data[0].last_given_at.timestamp())}:R>")
6163
footers.append(f"Tracked Messages: {levels_data[0].total_messages}")
6264
else:
6365
embed.add_field(name="Last Seen Here At", value="Not available")
6466
footers.append("Tracked Messages: N/A")
6567

66-
embed.add_field(name='Member Joined', value=member.joined_at.strftime(datetime_format), inline=True)
68+
embed.add_field(name='Member Joined', value=f"<t:{int(member.joined_at.timestamp())}:f>", inline=True)
6769
if member.premium_since is not None:
68-
embed.add_field(name='Member Boosted', value=member.premium_since.strftime(datetime_format), inline=True)
70+
embed.add_field(name='Member Boosted', value=f"<t:{int(member.premium_since.timestamp())}:f>", inline=True)
6971

7072
status = 'DND' if member.status is discord.Status.dnd else member.status.name.title()
7173
if member.status is not discord.Status.offline:
@@ -141,9 +143,8 @@ async def slash_role(self, ctx: SlashContext, role: discord.Role):
141143
@cooldown(1, 10, BucketType.channel)
142144
async def role(self, ctx: DozerContext, role: discord.Role):
143145
"""Retrieve info about a role in this guild"""
144-
embed = discord.Embed(title=f"Info for role: {role.name}", description=f"{role.mention} ({role.id})",
145-
color=role.color)
146-
embed.add_field(name="Created on", value=role.created_at.strftime(datetime_format))
146+
embed = discord.Embed(title=f"Info for role: {role.name}", description=f"{role.mention} ({role.id})", color=role.color)
147+
embed.add_field(name="Created on", value=f"<t:{int(role.created_at.timestamp())}:f>")
147148
embed.add_field(name="Position", value=role.position)
148149
embed.add_field(name="Color", value=str(role.color).upper())
149150
embed.add_field(name="Assigned members", value=f"{len(role.members)}", inline=False)
@@ -184,7 +185,7 @@ async def guild(self, ctx: DozerContext):
184185

185186
embed.set_thumbnail(url=guild.icon_url)
186187

187-
embed.add_field(name='Created at', value=guild.created_at.strftime(datetime_format))
188+
embed.add_field(name='Created on', value=f"<t:{int(guild.created_at.timestamp())}:f>")
188189
embed.add_field(name='Owner', value=guild.owner)
189190
embed.add_field(name='Emoji', value="{} static, {} animated".format(static_emoji, animated_emoji))
190191
embed.add_field(name='Roles', value=str(len(guild.roles) - 1)) # Remove @everyone

dozer/cogs/levels.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ async def checkrolelevels(self, ctx: DozerContext):
346346
roles = sorted(unsorted, key=lambda entry: entry.level) # Sort roles based on level
347347
embeds = []
348348

349-
for page_num, page in enumerate(chunk(roles.__iter__(), 10)):
349+
for page_num, page in enumerate(chunk(roles, 10)):
350350
e = discord.Embed(title=f"Level roles for {ctx.guild}", color=blurple)
351351
e.description = f"This server has {len(roles)} level roles"
352352
for level_role in page:

0 commit comments

Comments
 (0)