Skip to content

Commit 146c829

Browse files
committed
Add scripts/export-members.py
1 parent b16073a commit 146c829

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

scripts/export-members.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""Script to export all guild members and their roles to per-guild .csv files."""
2+
3+
import argparse
4+
import asyncio
5+
import csv
6+
import logging
7+
import os
8+
import sys
9+
from pathlib import Path
10+
11+
import discord
12+
from discord.ext.commands import Bot
13+
14+
DESCRIPTION = """\
15+
Export all guild members and their roles to per-guild .csv files.
16+
17+
Requires the environment variable 'BOT_TOKEN' to be set.
18+
Requires bot privileges for receiving 'GUILD_MEMBER' events.
19+
"""
20+
21+
22+
def report_error(message: str) -> None:
23+
"""Print an error message to stderr."""
24+
print("ERROR:", message, file=sys.stderr)
25+
26+
27+
def write_members_to_csv_file(guild: discord.Guild, output_file: Path) -> None:
28+
"""Write all guild members and their roles to a .csv files."""
29+
guild_roles = [role for role in guild.roles if role.name != "@everyone"]
30+
31+
entries = []
32+
for member in guild.members:
33+
member_role_ids = {role.id for role in member.roles if role.name != "@everyone"}
34+
entries.append(
35+
{
36+
"guild_id": guild.id,
37+
"guild_name": guild.name,
38+
"member_id": member.id,
39+
"member_name": member.name,
40+
"member_nickname": member.display_name,
41+
**{role.name: "x" if role.id in member_role_ids else "" for role in guild_roles},
42+
}
43+
)
44+
45+
with output_file.open("w") as fp:
46+
writer = csv.DictWriter(fp, fieldnames=entries[0].keys(), dialect="unix")
47+
writer.writeheader()
48+
writer.writerows(entries)
49+
50+
51+
class MemberExportBot(Bot):
52+
def __init__(self, output_dir: Path) -> None:
53+
"""Discord bot which exports all guild members to .csv files and then stops itself."""
54+
super().__init__(
55+
intents=discord.Intents(guilds=True, members=True),
56+
command_prefix="$",
57+
)
58+
59+
self.__output_dir = output_dir
60+
61+
async def on_ready(self) -> None:
62+
"""Event handler for successful connection."""
63+
self.__output_dir.mkdir(exist_ok=True)
64+
for guild in self.guilds:
65+
output_file = self.__output_dir / f"{guild.id}-members.csv"
66+
write_members_to_csv_file(guild, output_file)
67+
68+
await self.close()
69+
70+
async def on_error(self, event: str, *args, **kwargs) -> None:
71+
"""Event handler for uncaught exceptions."""
72+
exc_type, exc_value, _exc_traceback = sys.exc_info()
73+
report_error(f"{exc_type.__name__} {exc_value}")
74+
75+
# let discord.py log the exception
76+
await super().on_error(event, *args, **kwargs)
77+
78+
await self.close()
79+
80+
81+
async def run_bot(bot: Bot, token: str) -> None:
82+
"""Run a Discord bot."""
83+
async with bot as _bot:
84+
try:
85+
await _bot.login(token)
86+
await _bot.connect()
87+
except discord.LoginFailure:
88+
report_error("Invalid Discord bot token")
89+
except discord.PrivilegedIntentsRequired:
90+
report_error("Insufficient privileges. Required events: 'GUILD_MEMBERS'")
91+
92+
93+
def main():
94+
"""Run this application."""
95+
parser = argparse.ArgumentParser(
96+
description=DESCRIPTION,
97+
formatter_class=argparse.RawTextHelpFormatter,
98+
)
99+
parser.add_argument("output_dir", type=Path, help="Output directory")
100+
parser.add_argument("--debug", action="store_true", help="Enable logging")
101+
args = parser.parse_args()
102+
103+
bot_token = os.getenv("BOT_TOKEN")
104+
if bot_token is None:
105+
raise RuntimeError("'BOT_TOKEN' environment variable is not set")
106+
107+
if args.debug:
108+
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
109+
110+
bot = MemberExportBot(args.output_dir)
111+
asyncio.run(run_bot(bot, bot_token))
112+
113+
114+
if __name__ == "__main__":
115+
main()

0 commit comments

Comments
 (0)