discordcli-agents 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- discord_cli/__init__.py +3 -0
- discord_cli/__main__.py +6 -0
- discord_cli/cli.py +48 -0
- discord_cli/client.py +102 -0
- discord_cli/commands/__init__.py +1 -0
- discord_cli/commands/categories.py +93 -0
- discord_cli/commands/channels.py +198 -0
- discord_cli/commands/export.py +66 -0
- discord_cli/commands/guilds.py +239 -0
- discord_cli/commands/invites.py +84 -0
- discord_cli/commands/members.py +200 -0
- discord_cli/commands/messages.py +204 -0
- discord_cli/commands/permissions.py +121 -0
- discord_cli/commands/roles.py +193 -0
- discord_cli/commands/search.py +73 -0
- discord_cli/commands/threads.py +140 -0
- discord_cli/commands/webhooks.py +82 -0
- discord_cli/config.py +29 -0
- discord_cli/errors.py +17 -0
- discord_cli/models.py +290 -0
- discord_cli/output.py +57 -0
- discord_cli/registry.py +60 -0
- discordcli_agents-0.1.0.dist-info/METADATA +248 -0
- discordcli_agents-0.1.0.dist-info/RECORD +27 -0
- discordcli_agents-0.1.0.dist-info/WHEEL +4 -0
- discordcli_agents-0.1.0.dist-info/entry_points.txt +2 -0
- discordcli_agents-0.1.0.dist-info/licenses/LICENSE +21 -0
discord_cli/__init__.py
ADDED
discord_cli/__main__.py
ADDED
discord_cli/cli.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Command-line entry point for discord-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from discord_cli import __version__
|
|
8
|
+
|
|
9
|
+
# Import all command modules to register them
|
|
10
|
+
from discord_cli.commands import (
|
|
11
|
+
categories,
|
|
12
|
+
channels,
|
|
13
|
+
export,
|
|
14
|
+
guilds,
|
|
15
|
+
invites,
|
|
16
|
+
members,
|
|
17
|
+
messages,
|
|
18
|
+
permissions,
|
|
19
|
+
roles,
|
|
20
|
+
search,
|
|
21
|
+
threads,
|
|
22
|
+
webhooks,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
@click.version_option(version=__version__, prog_name="discord-cli")
|
|
28
|
+
def main() -> None:
|
|
29
|
+
"""Command-line tool for managing Discord servers and automating Discord via AI agents."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Register all command groups
|
|
33
|
+
main.add_command(channels.channel)
|
|
34
|
+
main.add_command(categories.category)
|
|
35
|
+
main.add_command(roles.role)
|
|
36
|
+
main.add_command(members.member)
|
|
37
|
+
main.add_command(messages.message)
|
|
38
|
+
main.add_command(guilds.guild)
|
|
39
|
+
main.add_command(permissions.permissions)
|
|
40
|
+
main.add_command(webhooks.webhook)
|
|
41
|
+
main.add_command(invites.invites)
|
|
42
|
+
main.add_command(threads.threads)
|
|
43
|
+
main.add_command(search.search)
|
|
44
|
+
main.add_command(export.export)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
discord_cli/client.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Discord bot lifecycle: connect, run a single action, print the result, exit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import AsyncIterator, Awaitable
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
import discord
|
|
11
|
+
|
|
12
|
+
from discord_cli.config import load_config
|
|
13
|
+
from discord_cli.errors import DiscordCliError
|
|
14
|
+
from discord_cli.output import make_payload, print_output
|
|
15
|
+
|
|
16
|
+
Action = Callable[..., Awaitable[Any]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ManagementClient(discord.Client):
|
|
20
|
+
"""A discord.Client that runs a single action on_ready and then closes."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, action: Action, kwargs: dict[str, Any], intents: discord.Intents):
|
|
23
|
+
super().__init__(intents=intents)
|
|
24
|
+
self.action = action
|
|
25
|
+
self.kwargs = kwargs
|
|
26
|
+
self.result: Any = None
|
|
27
|
+
self.error: dict[str, Any] | None = None
|
|
28
|
+
|
|
29
|
+
async def on_ready(self) -> None:
|
|
30
|
+
try:
|
|
31
|
+
self.result = await self.action(self, **self.kwargs)
|
|
32
|
+
except Exception as exc: # noqa: BLE001 - user-facing CLI boundary
|
|
33
|
+
self.error = {"type": exc.__class__.__name__, "message": str(exc)}
|
|
34
|
+
if isinstance(exc, discord.HTTPException):
|
|
35
|
+
self.error.update({"status": exc.status, "code": exc.code})
|
|
36
|
+
finally:
|
|
37
|
+
await self.close()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@asynccontextmanager
|
|
41
|
+
async def bot_session(client: ManagementClient, token: str) -> AsyncIterator[ManagementClient]:
|
|
42
|
+
"""Connect the client, let it run its action to completion, and guarantee it closes."""
|
|
43
|
+
try:
|
|
44
|
+
await client.start(token)
|
|
45
|
+
yield client
|
|
46
|
+
finally:
|
|
47
|
+
if not client.is_closed():
|
|
48
|
+
await client.close()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fallback_intents() -> discord.Intents:
|
|
52
|
+
intents = discord.Intents.default()
|
|
53
|
+
intents.guilds = True
|
|
54
|
+
intents.reactions = True
|
|
55
|
+
return intents
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _run_async(
|
|
59
|
+
action: Action, kwargs: dict[str, Any], token: str
|
|
60
|
+
) -> tuple[Any, dict[str, Any] | None]:
|
|
61
|
+
last_error: dict[str, Any] | None = None
|
|
62
|
+
for intents in (discord.Intents.all(), fallback_intents()):
|
|
63
|
+
client = ManagementClient(action, kwargs, intents)
|
|
64
|
+
try:
|
|
65
|
+
async with bot_session(client, token) as session:
|
|
66
|
+
result, error = session.result, session.error
|
|
67
|
+
except discord.PrivilegedIntentsRequired as exc:
|
|
68
|
+
last_error = {"type": exc.__class__.__name__, "message": str(exc)}
|
|
69
|
+
continue
|
|
70
|
+
except Exception as exc: # noqa: BLE001 - user-facing CLI boundary
|
|
71
|
+
return None, {"type": exc.__class__.__name__, "message": str(exc)}
|
|
72
|
+
if last_error and error is None and isinstance(result, dict):
|
|
73
|
+
result.setdefault(
|
|
74
|
+
"warning",
|
|
75
|
+
"Retried with non-privileged intents because privileged intents are not enabled for this bot.",
|
|
76
|
+
)
|
|
77
|
+
return result, error
|
|
78
|
+
return None, last_error
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run_bot(
|
|
82
|
+
action: Action, kwargs: dict[str, Any], token: str
|
|
83
|
+
) -> tuple[Any, dict[str, Any] | None]:
|
|
84
|
+
"""Run `action` against a freshly connected bot and return (result, error)."""
|
|
85
|
+
return asyncio.run(_run_async(action, kwargs, token))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_action(
|
|
89
|
+
action: Action, kwargs: dict[str, Any], human: bool, token: str | None = None
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Load config, run the action against the bot, print the payload, and exit with its status."""
|
|
92
|
+
try:
|
|
93
|
+
bot_token = load_config(token).bot_token
|
|
94
|
+
except DiscordCliError as exc:
|
|
95
|
+
payload = make_payload(error={"type": exc.__class__.__name__, "message": str(exc)})
|
|
96
|
+
print_output(payload, human)
|
|
97
|
+
raise SystemExit(1) from None
|
|
98
|
+
|
|
99
|
+
result, error = run_bot(action, kwargs, bot_token)
|
|
100
|
+
payload = make_payload(result, error)
|
|
101
|
+
print_output(payload, human)
|
|
102
|
+
raise SystemExit(0 if payload["ok"] else 1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command implementations for discord-cli, grouped by resource (channel, role, member, ...)."""
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Category commands: list, create, delete, edit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import discord
|
|
9
|
+
|
|
10
|
+
from discord_cli.errors import CliError
|
|
11
|
+
from discord_cli.models import channel_to_dict, get_category, get_guild
|
|
12
|
+
from discord_cli.registry import invoke, registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@registry.group("category")
|
|
16
|
+
def category_group() -> None:
|
|
17
|
+
"""Manage channel categories."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@category_group.command("list")
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def category_list(ctx: click.Context) -> None:
|
|
23
|
+
invoke(ctx, action_category_list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@category_group.command("create")
|
|
27
|
+
@click.argument("name")
|
|
28
|
+
@click.option("--position", type=int, default=None)
|
|
29
|
+
@click.pass_context
|
|
30
|
+
def category_create(ctx: click.Context, name: str, position: int | None) -> None:
|
|
31
|
+
invoke(ctx, action_category_create, name=name, position=position)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@category_group.command("delete")
|
|
35
|
+
@click.argument("category_id", type=int)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def category_delete(ctx: click.Context, category_id: int) -> None:
|
|
38
|
+
invoke(ctx, action_category_delete, category_id=category_id)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@category_group.command("edit")
|
|
42
|
+
@click.argument("category_id", type=int)
|
|
43
|
+
@click.option("--name", default=None)
|
|
44
|
+
@click.option("--position", type=int, default=None)
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def category_edit(
|
|
47
|
+
ctx: click.Context, category_id: int, name: str | None, position: int | None
|
|
48
|
+
) -> None:
|
|
49
|
+
invoke(ctx, action_category_edit, category_id=category_id, name=name, position=position)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def action_category_list(client: discord.Client, guild_id: int | None, **_: Any) -> Any:
|
|
53
|
+
guild = await get_guild(client, guild_id)
|
|
54
|
+
return {
|
|
55
|
+
"guild_id": guild.id,
|
|
56
|
+
"categories": [channel_to_dict(c, detailed=True) for c in guild.categories],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def action_category_create(
|
|
61
|
+
client: discord.Client, guild_id: int | None, name: str, position: int | None, **_: Any
|
|
62
|
+
) -> Any:
|
|
63
|
+
guild = await get_guild(client, guild_id)
|
|
64
|
+
category = await guild.create_category(
|
|
65
|
+
name, position=position, reason="discord_cli category create"
|
|
66
|
+
)
|
|
67
|
+
return {"created": channel_to_dict(category, detailed=True)}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def action_category_delete(
|
|
71
|
+
client: discord.Client, guild_id: int | None, category_id: int, **_: Any
|
|
72
|
+
) -> Any:
|
|
73
|
+
guild = await get_guild(client, guild_id)
|
|
74
|
+
category = get_category(guild, category_id)
|
|
75
|
+
data = channel_to_dict(category)
|
|
76
|
+
await category.delete(reason="discord_cli category delete")
|
|
77
|
+
return {"deleted": data}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def action_category_edit(
|
|
81
|
+
client: discord.Client, guild_id: int | None, category_id: int, **kwargs: Any
|
|
82
|
+
) -> Any:
|
|
83
|
+
guild = await get_guild(client, guild_id)
|
|
84
|
+
category = get_category(guild, category_id)
|
|
85
|
+
edits = {
|
|
86
|
+
k: v
|
|
87
|
+
for k, v in {"name": kwargs.get("name"), "position": kwargs.get("position")}.items()
|
|
88
|
+
if v is not None
|
|
89
|
+
}
|
|
90
|
+
if not edits:
|
|
91
|
+
raise CliError("No edits provided")
|
|
92
|
+
await category.edit(**edits, reason="discord_cli category edit")
|
|
93
|
+
return {"updated": channel_to_dict(category, detailed=True)}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Channel commands: list, create, delete, edit, move, info."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import discord
|
|
9
|
+
|
|
10
|
+
from discord_cli.errors import CliError
|
|
11
|
+
from discord_cli.models import channel_to_dict, get_category, get_channel, get_guild
|
|
12
|
+
from discord_cli.registry import TRI_STATE_BOOL, invoke, registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@registry.group("channel")
|
|
16
|
+
def channel_group() -> None:
|
|
17
|
+
"""Manage guild channels."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@channel_group.command("list")
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def channel_list(ctx: click.Context) -> None:
|
|
23
|
+
invoke(ctx, action_channel_list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@channel_group.command("create")
|
|
27
|
+
@click.argument("name")
|
|
28
|
+
@click.option(
|
|
29
|
+
"--type", "channel_type", type=click.Choice(["text", "voice", "stage", "forum"]), default="text"
|
|
30
|
+
)
|
|
31
|
+
@click.option("--category", type=int, default=None)
|
|
32
|
+
@click.option("--topic", default=None)
|
|
33
|
+
@click.option("--position", type=int, default=None)
|
|
34
|
+
@click.option("--nsfw", is_flag=True, default=False)
|
|
35
|
+
@click.option("--slowmode", type=int, default=None)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def channel_create(
|
|
38
|
+
ctx: click.Context,
|
|
39
|
+
name: str,
|
|
40
|
+
channel_type: str,
|
|
41
|
+
category: int | None,
|
|
42
|
+
topic: str | None,
|
|
43
|
+
position: int | None,
|
|
44
|
+
nsfw: bool,
|
|
45
|
+
slowmode: int | None,
|
|
46
|
+
) -> None:
|
|
47
|
+
invoke(
|
|
48
|
+
ctx,
|
|
49
|
+
action_channel_create,
|
|
50
|
+
name=name,
|
|
51
|
+
type=channel_type,
|
|
52
|
+
category=category,
|
|
53
|
+
topic=topic,
|
|
54
|
+
position=position,
|
|
55
|
+
nsfw=nsfw,
|
|
56
|
+
slowmode=slowmode,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@channel_group.command("delete")
|
|
61
|
+
@click.argument("channel_id", type=int)
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def channel_delete(ctx: click.Context, channel_id: int) -> None:
|
|
64
|
+
invoke(ctx, action_channel_delete, channel_id=channel_id)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@channel_group.command("edit")
|
|
68
|
+
@click.argument("channel_id", type=int)
|
|
69
|
+
@click.option("--name", default=None)
|
|
70
|
+
@click.option("--topic", default=None)
|
|
71
|
+
@click.option("--position", type=int, default=None)
|
|
72
|
+
@click.option("--slowmode", type=int, default=None)
|
|
73
|
+
@click.option("--nsfw", type=TRI_STATE_BOOL, default=None)
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def channel_edit(
|
|
76
|
+
ctx: click.Context,
|
|
77
|
+
channel_id: int,
|
|
78
|
+
name: str | None,
|
|
79
|
+
topic: str | None,
|
|
80
|
+
position: int | None,
|
|
81
|
+
slowmode: int | None,
|
|
82
|
+
nsfw: bool | None,
|
|
83
|
+
) -> None:
|
|
84
|
+
invoke(
|
|
85
|
+
ctx,
|
|
86
|
+
action_channel_edit,
|
|
87
|
+
channel_id=channel_id,
|
|
88
|
+
name=name,
|
|
89
|
+
topic=topic,
|
|
90
|
+
position=position,
|
|
91
|
+
slowmode=slowmode,
|
|
92
|
+
nsfw=nsfw,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@channel_group.command("move")
|
|
97
|
+
@click.argument("channel_id", type=int)
|
|
98
|
+
@click.option("--category", type=int, required=True)
|
|
99
|
+
@click.pass_context
|
|
100
|
+
def channel_move(ctx: click.Context, channel_id: int, category: int) -> None:
|
|
101
|
+
invoke(ctx, action_channel_move, channel_id=channel_id, category=category)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@channel_group.command("info")
|
|
105
|
+
@click.argument("channel_id", type=int)
|
|
106
|
+
@click.pass_context
|
|
107
|
+
def channel_info(ctx: click.Context, channel_id: int) -> None:
|
|
108
|
+
invoke(ctx, action_channel_info, channel_id=channel_id)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def action_channel_list(client: discord.Client, guild_id: int | None, **_: Any) -> Any:
|
|
112
|
+
guild = await get_guild(client, guild_id)
|
|
113
|
+
return {"guild_id": guild.id, "channels": [channel_to_dict(c) for c in guild.channels]}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def action_channel_create(client: discord.Client, guild_id: int | None, **kwargs: Any) -> Any:
|
|
117
|
+
guild = await get_guild(client, guild_id)
|
|
118
|
+
category = get_category(guild, kwargs.get("category"))
|
|
119
|
+
common = {
|
|
120
|
+
"category": category,
|
|
121
|
+
"position": kwargs.get("position"),
|
|
122
|
+
"reason": "discord_cli channel create",
|
|
123
|
+
}
|
|
124
|
+
common = {k: v for k, v in common.items() if v is not None}
|
|
125
|
+
ctype = kwargs.get("type") or "text"
|
|
126
|
+
name = kwargs["name"]
|
|
127
|
+
if ctype == "text":
|
|
128
|
+
channel = await guild.create_text_channel(
|
|
129
|
+
name,
|
|
130
|
+
topic=kwargs.get("topic"),
|
|
131
|
+
nsfw=kwargs.get("nsfw", False),
|
|
132
|
+
slowmode_delay=kwargs.get("slowmode") or 0,
|
|
133
|
+
**common,
|
|
134
|
+
)
|
|
135
|
+
elif ctype == "voice":
|
|
136
|
+
channel = await guild.create_voice_channel(name, **common)
|
|
137
|
+
elif ctype == "stage":
|
|
138
|
+
channel = await guild.create_stage_channel(name, **common)
|
|
139
|
+
elif ctype == "forum":
|
|
140
|
+
channel = await guild.create_forum_channel(
|
|
141
|
+
name,
|
|
142
|
+
topic=kwargs.get("topic"),
|
|
143
|
+
nsfw=kwargs.get("nsfw", False),
|
|
144
|
+
slowmode_delay=kwargs.get("slowmode") or 0,
|
|
145
|
+
**common,
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
raise CliError(f"Unsupported channel type: {ctype}")
|
|
149
|
+
return {"created": channel_to_dict(channel, detailed=True)}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def action_channel_delete(
|
|
153
|
+
client: discord.Client, guild_id: int | None, channel_id: int, **_: Any
|
|
154
|
+
) -> Any:
|
|
155
|
+
guild = await get_guild(client, guild_id)
|
|
156
|
+
channel = get_channel(guild, channel_id)
|
|
157
|
+
data = channel_to_dict(channel)
|
|
158
|
+
await channel.delete(reason="discord_cli channel delete")
|
|
159
|
+
return {"deleted": data}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def action_channel_edit(
|
|
163
|
+
client: discord.Client, guild_id: int | None, channel_id: int, **kwargs: Any
|
|
164
|
+
) -> Any:
|
|
165
|
+
guild = await get_guild(client, guild_id)
|
|
166
|
+
channel = get_channel(guild, channel_id)
|
|
167
|
+
edits: dict[str, Any] = {}
|
|
168
|
+
if kwargs.get("name") is not None:
|
|
169
|
+
edits["name"] = kwargs["name"]
|
|
170
|
+
if kwargs.get("topic") is not None and hasattr(channel, "topic"):
|
|
171
|
+
edits["topic"] = kwargs["topic"]
|
|
172
|
+
if kwargs.get("position") is not None:
|
|
173
|
+
edits["position"] = kwargs["position"]
|
|
174
|
+
if kwargs.get("slowmode") is not None and hasattr(channel, "slowmode_delay"):
|
|
175
|
+
edits["slowmode_delay"] = kwargs["slowmode"]
|
|
176
|
+
if kwargs.get("nsfw") is not None and hasattr(channel, "nsfw"):
|
|
177
|
+
edits["nsfw"] = kwargs["nsfw"]
|
|
178
|
+
if not edits:
|
|
179
|
+
raise CliError("No edits provided")
|
|
180
|
+
edited = await channel.edit(**edits, reason="discord_cli channel edit")
|
|
181
|
+
return {"updated": channel_to_dict(edited or channel, detailed=True)}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def action_channel_move(
|
|
185
|
+
client: discord.Client, guild_id: int | None, channel_id: int, category: int, **_: Any
|
|
186
|
+
) -> Any:
|
|
187
|
+
guild = await get_guild(client, guild_id)
|
|
188
|
+
channel = get_channel(guild, channel_id)
|
|
189
|
+
new_category = get_category(guild, category)
|
|
190
|
+
await channel.edit(category=new_category, reason="discord_cli channel move")
|
|
191
|
+
return {"updated": channel_to_dict(channel, detailed=True)}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def action_channel_info(
|
|
195
|
+
client: discord.Client, guild_id: int | None, channel_id: int, **_: Any
|
|
196
|
+
) -> Any:
|
|
197
|
+
guild = await get_guild(client, guild_id)
|
|
198
|
+
return channel_to_dict(get_channel(guild, channel_id), detailed=True)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Export commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from ..client import run_bot
|
|
11
|
+
from ..output import output
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
def export() -> None:
|
|
16
|
+
"""Export channel data."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@export.command("channel")
|
|
20
|
+
@click.argument("channel_id", type=int)
|
|
21
|
+
@click.option("--limit", type=int, default=100, help="Number of messages to export")
|
|
22
|
+
@click.option("--format", type=click.Choice(["json", "csv"]), default="json")
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def export_channel(ctx: click.Context, channel_id: int, limit: int, fmt: str) -> None:
|
|
25
|
+
"""Export messages from a channel."""
|
|
26
|
+
|
|
27
|
+
async def _action(client, **kwargs):
|
|
28
|
+
guild = client.guilds[0]
|
|
29
|
+
channel = guild.get_channel(channel_id)
|
|
30
|
+
if not channel:
|
|
31
|
+
raise ValueError(f"Channel {channel_id} not found")
|
|
32
|
+
|
|
33
|
+
messages = []
|
|
34
|
+
async for msg in channel.history(limit=limit):
|
|
35
|
+
messages.append(
|
|
36
|
+
{
|
|
37
|
+
"id": msg.id,
|
|
38
|
+
"author_id": msg.author.id,
|
|
39
|
+
"author_name": msg.author.name,
|
|
40
|
+
"content": msg.content,
|
|
41
|
+
"created_at": msg.created_at.isoformat(),
|
|
42
|
+
"edited_at": msg.edited_at.isoformat() if msg.edited_at else None,
|
|
43
|
+
"attachments": [
|
|
44
|
+
{"url": a.url, "filename": a.filename, "content_type": a.content_type}
|
|
45
|
+
for a in msg.attachments
|
|
46
|
+
],
|
|
47
|
+
"embeds": len(msg.embeds),
|
|
48
|
+
"pinned": msg.pinned,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if fmt == "csv":
|
|
53
|
+
output_buf = io.StringIO()
|
|
54
|
+
writer = csv.DictWriter(
|
|
55
|
+
output_buf,
|
|
56
|
+
fieldnames=["id", "author_id", "author_name", "content", "created_at", "pinned"],
|
|
57
|
+
)
|
|
58
|
+
writer.writeheader()
|
|
59
|
+
for m in messages:
|
|
60
|
+
writer.writerow({k: m.get(k, "") for k in writer.fieldnames})
|
|
61
|
+
return {"format": "csv", "count": len(messages), "data": output_buf.getvalue()}
|
|
62
|
+
else:
|
|
63
|
+
return {"format": "json", "count": len(messages), "messages": messages}
|
|
64
|
+
|
|
65
|
+
result, error = run_bot(_action, ctx=ctx)
|
|
66
|
+
output(result, error, ctx)
|