├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── build.zig
├── src
├── channel.zig
├── discord.zig
├── guild.zig
└── main.zig
├── zig.mod
├── zigmod.lock
└── zigmod.sum
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.zigmod
2 | /deps.zig
3 | /zig-*
4 | /*.txt
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU Affero General Public License v3.0
2 |
3 | Copyright (c) 2021 Meghan Denny
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 |
18 | https://www.gnu.org/licenses/agpl-3.0.txt
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # discord-archiver
2 | 
3 | [](https://github.com/nektro/discord-archiver/blob/master/LICENSE)
4 | [](https://discord.gg/P6Y4zQC)
5 |
6 |
7 |
8 |
9 | An archiver for Discord. Written in Zig.
10 |
11 | ## Usage
12 | 1. Channels
13 | ```
14 | $ ./discord-archiver channel
15 | ```
16 |
17 | 2. Guilds
18 | ```
19 | $ ./discord-archiver guild
20 | ```
21 |
22 | ## Zig
23 | - https://ziglang.org/
24 | - https://github.com/ziglang/zig
25 | - https://github.com/ziglang/zig/wiki/Community
26 |
27 | ## Building
28 | ```
29 | $ zigmod fetch
30 | $ zig build
31 | ```
32 |
33 | ## Built With
34 | - Zig Master & [Zigmod Package Manager](https://github.com/nektro/zigmod)
35 | - https://github.com/nektro/zig-ansi
36 | - https://github.com/truemedian/zfetch
37 |
38 | ## License
39 | AGPL-3.0
40 |
--------------------------------------------------------------------------------
/build.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const deps = @import("./deps.zig");
3 |
4 | pub fn build(b: *std.build.Builder) void {
5 | const target = b.standardTargetOptions(.{});
6 |
7 | const mode = b.standardReleaseOptions();
8 |
9 | const exe = b.addExecutable("discord-archiver", "src/main.zig");
10 | exe.setTarget(target);
11 | exe.setBuildMode(mode);
12 | deps.addAllTo(exe);
13 | exe.install();
14 |
15 | const run_cmd = exe.run();
16 | run_cmd.step.dependOn(b.getInstallStep());
17 | if (b.args) |args| {
18 | run_cmd.addArgs(args);
19 | }
20 |
21 | const run_step = b.step("run", "Run the app");
22 | run_step.dependOn(&run_cmd.step);
23 | }
24 |
--------------------------------------------------------------------------------
/src/channel.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const json = @import("json");
3 | const range = @import("range").range;
4 |
5 | const discord = @import("./discord.zig");
6 |
7 | pub fn execute(alloc: *std.mem.Allocator, args: [][]u8) !void {
8 | const bot_token = args[0];
9 |
10 | for (range(args.len - 1)) |_, i| {
11 | try do(alloc, bot_token, args[i + 1]);
12 | }
13 | }
14 |
15 | pub fn do(alloc: *std.mem.Allocator, bot_token: []const u8, channel_id: []const u8) !void {
16 | const channel = try discord.get_channel(alloc, bot_token, channel_id);
17 | if (channel.?.get("message")) |_| {
18 | std.log.warn("{}", .{channel});
19 | return;
20 | }
21 | const name = channel.?.get("name").?.String;
22 | std.log.info("now backing up channel: {s} {s}", .{ channel_id, name });
23 |
24 | const save_path = try std.mem.join(alloc, "", &.{ channel_id, "_", name, ".txt" });
25 |
26 | const f = try std.fs.cwd().createFile(save_path, .{ .read = true, .truncate = false });
27 | defer f.close();
28 | const r = f.reader();
29 | const w = f.writer();
30 |
31 | const message_list = &std.ArrayList(json.Value).init(alloc);
32 | defer message_list.deinit();
33 |
34 | if ((try f.getEndPos()) == 0) {
35 | // full sync
36 | var next_flake: []const u8 = "";
37 | while (try discord.get_channel_messages(alloc, bot_token, channel_id, .before, next_flake)) |messages| {
38 | if (messages.len == 0) break;
39 | std.log.info("found {} messages starting at {s}, {} total so far", .{ messages.len, messages[0].get("id").?.String, message_list.items.len + messages.len });
40 | try message_list.appendSlice(messages);
41 | if (messages.len < 50) break;
42 | std.time.sleep(std.time.ns_per_s);
43 | next_flake = messages[messages.len - 1].get("id").?.String;
44 | }
45 | } else {
46 | // only append new messages
47 | std.log.info("backup has already been done", .{});
48 | var last_line: []const u8 = "";
49 | while (try r.readUntilDelimiterOrEofAlloc(alloc, '\n', std.math.maxInt(usize))) |line| {
50 | last_line = line;
51 | }
52 | const val = try json.parse(alloc, last_line);
53 | const id = val.get("id").?.String;
54 | std.log.info("last saved id is {s}", .{id});
55 |
56 | try f.seekTo(try f.getEndPos());
57 | var next_flake = id;
58 | while (try discord.get_channel_messages(alloc, bot_token, channel_id, .after, next_flake)) |messages| {
59 | if (messages.len == 0) break;
60 | std.log.info("found {} messages starting at {s}, {} total so far", .{ messages.len, messages[0].get("id").?.String, message_list.items.len + messages.len });
61 | try message_list.appendSlice(messages);
62 | if (messages.len < 50) break;
63 | std.time.sleep(std.time.ns_per_s);
64 | next_flake = messages[messages.len - 1].get("id").?.String;
65 | }
66 | }
67 |
68 | std.log.info("saving {} messages to disk", .{message_list.items.len});
69 | for (range(message_list.items.len)) |_, i| {
70 | try w.print("{}\n", .{message_list.items[message_list.items.len - 1 - i]});
71 | }
72 |
73 | std.log.info("done", .{});
74 | }
75 |
--------------------------------------------------------------------------------
/src/discord.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const zfetch = @import("zfetch");
3 | const json = @import("json");
4 |
5 | pub const API_ROOT = "https://discord.com/api";
6 |
7 | //
8 | fn do_discord_request(alloc: *std.mem.Allocator, method: zfetch.Method, url: []const u8, bot_token: []const u8) !json.Value {
9 | const req = try zfetch.Request.init(alloc, url, null);
10 | defer req.deinit();
11 |
12 | var headers = zfetch.Headers.init(alloc);
13 | defer headers.deinit();
14 | try headers.appendValue("Authorization", try std.mem.join(alloc, " ", &.{ "Bot", bot_token }));
15 |
16 | try req.do(method, headers, null);
17 | const r = req.reader();
18 |
19 | const body_content = try r.readAllAlloc(alloc, std.math.maxInt(usize));
20 | const val = json.parse(alloc, body_content) catch |e| {
21 | std.log.alert("caught error: {}, dumping response content", .{e});
22 | std.log.alert("{s}", .{body_content});
23 | return json.Value{ .Null = void{} };
24 | };
25 | return val;
26 | }
27 | //
28 |
29 | // https://discord.com/developers/docs/resources/guild#get-guild
30 | pub fn get_guild(alloc: *std.mem.Allocator, bot_token: []const u8, guild_id: []const u8) !?json.Value {
31 | const url = try std.mem.join(alloc, "/", &.{ API_ROOT, "guilds", guild_id });
32 | const val = try do_discord_request(alloc, .GET, url, bot_token);
33 | return val;
34 | }
35 |
36 | // https://discord.com/developers/docs/resources/guild#get-guild-channels
37 | // GET/guilds/{guild.id}/channels
38 | pub fn get_guild_channels(alloc: *std.mem.Allocator, bot_token: []const u8, guild_id: []const u8) !?[]json.Value {
39 | const url = try std.mem.join(alloc, "/", &.{ API_ROOT, "guilds", guild_id, "channels" });
40 | const val = try do_discord_request(alloc, .GET, url, bot_token);
41 | if (val != .Array) {
42 | std.log.err("got non array type from discord", .{});
43 | std.log.err("{}", .{val});
44 | return null;
45 | }
46 | return val.Array;
47 | }
48 |
49 | // https://discord.com/developers/docs/resources/channel#get-channel
50 | pub fn get_channel(alloc: *std.mem.Allocator, bot_token: []const u8, channel_id: []const u8) !?json.Value {
51 | const url = try std.mem.join(alloc, "/", &.{ API_ROOT, "channels", channel_id });
52 | const val = try do_discord_request(alloc, .GET, url, bot_token);
53 | return val;
54 | }
55 |
56 | // https://discord.com/developers/docs/resources/channel#get-channel-messages
57 | pub fn get_channel_messages(alloc: *std.mem.Allocator, bot_token: []const u8, channel_id: []const u8, direction: enum { before, after }, flake: []const u8) !?[]json.Value {
58 | var url = try std.mem.join(alloc, "/", &.{ API_ROOT, "channels", channel_id, "messages" });
59 | if (flake.len > 0) {
60 | url = try std.mem.join(alloc, "", &.{ url, "?", std.meta.tagName(direction), "=", flake });
61 | }
62 | const val = try do_discord_request(alloc, .GET, url, bot_token);
63 | if (val != .Array) {
64 | std.log.err("got non array type from discord", .{});
65 | std.log.err("{}", .{val});
66 | return null;
67 | }
68 | return val.Array;
69 | }
70 |
--------------------------------------------------------------------------------
/src/guild.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const zfetch = @import("zfetch");
3 | const json = @import("json");
4 |
5 | const discord = @import("./discord.zig");
6 | const channel = @import("./channel.zig");
7 |
8 | pub fn execute(alloc: *std.mem.Allocator, args: [][]u8) !void {
9 | const bot_token = args[0];
10 | const guild_id = args[1];
11 |
12 | const guild = try discord.get_guild(alloc, bot_token, guild_id);
13 | if (guild.?.get("message")) |_| {
14 | std.log.warn("{}", .{guild});
15 | return;
16 | }
17 | const name = guild.?.get("name").?.String;
18 | std.log.info("now backing up guild: {s} {s}", .{ guild_id, name });
19 |
20 | if (try discord.get_guild_channels(alloc, bot_token, guild_id)) |response| {
21 | for (response) |chan| {
22 | const channel_id = chan.get("id").?.String;
23 | const ctype = chan.get("type").?.Int;
24 | if (ctype != 0) {
25 | continue;
26 | }
27 | try channel.do(alloc, bot_token, channel_id);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const ansi = @import("ansi");
3 |
4 | pub fn main() !void {
5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
6 | const alloc = &gpa.allocator;
7 |
8 | const proc_args = try std.process.argsAlloc(alloc);
9 | const args = proc_args[1..];
10 |
11 | if (args.len == 0) {
12 | std.debug.print(ansi.style.FgRed, .{});
13 | defer std.debug.print(ansi.style.ResetFgColor, .{});
14 |
15 | std.log.info("must use a command", .{});
16 | std.log.info("the available commands are:", .{});
17 | std.log.info("\tguild BOT_TOKEN GUILD_ID", .{});
18 | std.log.info("\tchannel BOT_TOKEN CHANNEL_ID...", .{});
19 | return;
20 | }
21 |
22 | inline for (std.meta.declarations(commands)) |decl| {
23 | if (std.mem.eql(u8, args[0], decl.name)) {
24 | const cmd = @field(commands, decl.name);
25 | try cmd.execute(alloc, args[1..]);
26 | return;
27 | }
28 | }
29 |
30 | std.debug.print(ansi.style.FgRed, .{});
31 | std.log.err("command '{s}' not found", .{args[0]});
32 | std.debug.print(ansi.style.ResetFgColor, .{});
33 | }
34 |
35 | pub const commands = struct {
36 | pub const guild = @import("./guild.zig");
37 | pub const channel = @import("./channel.zig");
38 | };
39 |
--------------------------------------------------------------------------------
/zig.mod:
--------------------------------------------------------------------------------
1 | id: c18kxcuuq3ggftftmofowagbraksbbw68kux3uruvog7rbmn
2 | name: discord-archiver
3 | license: AGPL-3.0
4 | description: Chat archiver for Discord
5 | bin: True
6 | provides: [ "discord-archiver" ]
7 | dev_dependencies:
8 | - src: git https://github.com/nektro/zig-ansi
9 | - src: git https://github.com/truemedian/zfetch
10 | - src: git https://github.com/nektro/zig-json
11 | - src: git https://github.com/nektro/zig-range
12 |
--------------------------------------------------------------------------------
/zigmod.lock:
--------------------------------------------------------------------------------
1 | 2
2 | git https://github.com/nektro/zig-ansi commit-d4a53bcac5b87abecc65491109ec22aaf5f3dc2f
3 | git https://github.com/truemedian/zfetch commit-6ba2ba136ec7cfc887811039cd4a7d8a43ba725b
4 | git https://github.com/truemedian/hzzp commit-492107d44caa2676c7b5aa4e934e1e937232d652
5 | git https://github.com/alexnask/iguanaTLS commit-0d39a361639ad5469f8e4dcdaea35446bbe54b48
6 | git https://github.com/MasterQ32/zig-network commit-b9c91769d8ebd626c8e45b2abb05cbc28ccc50da
7 | git https://github.com/MasterQ32/zig-uri commit-52cdd2061bec0579519f0d30280597f3a1db8b75
8 | git https://github.com/nektro/zig-json commit-72e555fbc0776f2600aee19b01e5ab1855ebec7a
9 | git https://github.com/nektro/zig-range commit-890ca308fe09b3d5c866d5cfb3b3d7a95dbf939f
10 |
--------------------------------------------------------------------------------
/zigmod.sum:
--------------------------------------------------------------------------------
1 | blake3-7fc0b46397932ea1f0726d42289606ca118cc745d88dd87c0d6a377ba7c6569f git/github.com/nektro/zig-ansi
2 | blake3-d7996d9432e92afdb399c51b3666cbbcf469ba9dcc8fbdba59274f5dc9cc7fa2 git/github.com/truemedian/zfetch
3 | blake3-98982125d0fbedc62e179e62081d2797a2b8a3623c42f9fd5d72cd56d6350714 git/github.com/truemedian/hzzp
4 | blake3-e6901bd7432450d5b22b01880cc7fa3fa2433e766a527206f18b29c67c1349bb git/github.com/alexnask/iguanaTLS
5 | blake3-21f91e48333ac0ca7f4704c96352831c25216e7056d02ce24de95d03fc942246 git/github.com/MasterQ32/zig-network
6 | blake3-030ebb03f1ed21122e681b06786bea6f2f1b810e8eb9f2029d0eee4f4fb3103f git/github.com/MasterQ32/zig-uri
7 | blake3-1893709ffc6359c5f9cd2f9409abccf78a94ed37bb2c6dd075c603356d17c94b git/github.com/nektro/zig-json
8 | blake3-09698753782139ab4877d08f33235170836f68b73e482b65cdee5637a6addf86 git/github.com/nektro/zig-range
9 |
--------------------------------------------------------------------------------