1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use crate::{
    checks::ENABLED_CHECK,
    util::LoadingReaction,
    ClientDataKey,
};
use anyhow::{
    bail,
    Context as _,
};
use bytes::Bytes;
use insta::MediaType;
use serenity::{
    builder::{
        CreateAttachment,
        CreateMessage,
    },
    framework::standard::{
        macros::command,
        Args,
        CommandResult,
    },
    model::prelude::*,
    prelude::*,
};
use tracing::info;
use url::Url;

#[command("insta-dl")]
#[description("Download an instagram video or photo")]
#[usage("<url>")]
#[example("https://www.instagram.com/p/CIlZpXKFfNt/")]
#[checks(Enabled)]
#[min_args(1)]
#[max_args(1)]
#[bucket("insta-dl")]
async fn insta_dl(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
    let data_lock = ctx.data.read().await;
    let client_data = data_lock
        .get::<ClientDataKey>()
        .expect("missing client data");
    let client = client_data.insta_client.clone();
    drop(data_lock);

    let url = args.trimmed().current().expect("missing url");

    info!("downloading instagram post \"{url}\"");
    let mut loading = LoadingReaction::new(ctx.http.clone(), msg);

    let result = async {
        let post = client
            .get_post(url)
            .await
            .context("failed to get instagram post")?;
        download_post(&client.client, &post)
            .await
            .context("failed to download post")
    }
    .await;

    match result {
        Ok((post_data, file_name)) => {
            let file_builder = CreateAttachment::bytes(post_data, file_name);
            let message_builder = CreateMessage::new().add_file(file_builder);

            msg.channel_id
                .send_message(&ctx.http, message_builder)
                .await?;
            loading.send_ok();
        }
        Err(error) => {
            msg.channel_id.say(&ctx.http, format!("{error:?}")).await?;
        }
    }

    Ok(())
}

// TODO: Cache results
/// Download an instagram post
async fn download_post<'a>(
    client: &reqwest::Client,
    post_page: &'a insta::AdditionalDataLoaded,
) -> anyhow::Result<(Bytes, String)> {
    let post_page_item = post_page.items.first().context("missing post item")?;

    let url = match post_page_item.media_type {
        MediaType::Photo => {
            let image_versions2_candidate = post_page_item
                .get_best_image_versions2_candidate()
                .context("failed to select an image_versions2_candidate")?;
            &image_versions2_candidate.url
        }
        MediaType::Video => {
            let video_version = post_page_item
                .get_best_video_version()
                .context("failed to get the best video version")?;

            &video_version.url
        }
        media_type => {
            bail!("Unsupported media type `{media_type:?}`",);
        }
    };

    let code = &post_page_item.code;
    let extension = get_extension_from_url(url).context("missing image extension")?;
    let file_name = format!("{code}.{extension}");

    let data = client
        .get(url.as_str())
        .send()
        .await?
        .error_for_status()?
        .bytes()
        .await?;

    Ok((data, file_name))
}

/// Get the file extension from a url
fn get_extension_from_url(url: &Url) -> Option<&str> {
    Some(url.path_segments()?.next_back()?.rsplit_once('.')?.1)
}