<|start_filename|>Dockerfile<|end_filename|>
# Build
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /source

COPY . .

RUN dotnet restore
RUN dotnet publish -c release -o /app --no-restore

# Run
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "ExportAPI.dll"]


<|start_filename|>ExportAPI/Controllers/v2/ExportController.cs<|end_filename|>
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Core.Exporting.Partitioning;
using DiscordChatExporter.Core.Exporting.Filtering;
using DiscordChatExporter.Core.Discord.Data;

namespace ExportAPI.Controllers
{
	[ApiController]
	[Route("v2/export")]
	public class ExportController : ControllerBase
	{
		private readonly ILogger<ExportController> _logger;

		private readonly HttpClient _httpclient = new HttpClient();

		public ExportController(ILogger<ExportController> logger)
		{
			_logger = logger;

			if (!Directory.Exists("/tmp/exports"))
			{
				Directory.CreateDirectory("/tmp/exports");
			}

			var nowhex = DateTime.Now.Ticks.ToString("X2");
			if (!Directory.Exists($"/tmp/exports/{nowhex}"))
			{
				Directory.CreateDirectory($"/tmp/exports/{nowhex}");
			}
		}

		internal static string GetPath(string channelId, ExportFormat exportType)
		{
			var nowhex = DateTime.Now.Ticks.ToString("X2");
			return $"/tmp/exports/{nowhex}/{channelId}.{exportType.GetFileExtension()}";
		}

		[HttpPost]
		public async Task<Stream> Post(ExportOptions options)
		{
			Stream SendJsonError(string error, int status)
			{
				Response.ContentType = "application/json";
				Response.StatusCode = status;
				return GenerateStreamFromString("{ \"error\": \"" + error + "\" }");
			}

			if (!Enum.IsDefined(typeof(ExportFormat), options.export_format))
			{
				return SendJsonError($"An export format with the id '{options.export_format}' was not found.", 400);
			}

			var parsed = Snowflake.TryParse(options.channel_id);
			var channelId = parsed ?? Snowflake.Zero;

			var token = new AuthToken(AuthTokenKind.Bot, options.token);
			var client = new DiscordClient(token);
			Channel channel;
			try
			{
				channel = await client.GetChannelAsync(channelId);
			}
			catch (DiscordChatExporterException e)
			{
				if (e.Message.Contains("Authentication")) return SendJsonError("An invalid Discord token was provided.", 401);
				if (e.Message.Contains("Requested resource does not exist")) return SendJsonError("A channel with the provided ID was not found.", 404);
				return SendJsonError($"An unknown error occurred: {e.Message}", 500);
			}

			var guild = await client.GetGuildAsync(channel.GuildId);

			using var req = new HttpRequestMessage(HttpMethod.Get, new Uri("https://discord.com/api/v8/users/@me"));
			req.Headers.Authorization = token.GetAuthenticationHeader();
			var res = await _httpclient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
			var text = await res.Content.ReadAsStringAsync();
			var me = DiscordChatExporter.Core.Discord.Data.User.Parse(JsonDocument.Parse(text).RootElement.Clone());

			var path = GetPath(channel.Id.ToString(), options.export_format);
			_logger.LogInformation($"[{me.FullName} ({me.Id})] Exporting #{channel.Name} ({channel.Id}) within {guild.Name} ({guild.Id}) to {path}");
			var request = new ExportRequest(
				guild,
				channel,
				path,
				options.export_format,
				Snowflake.TryParse(options.after),
				Snowflake.TryParse(options.before),
				PartitionLimit.Null,
				MessageFilter.Null,
				false,
				false,
				options.date_format
			);

			var exporter = new ChannelExporter(client);

			_logger.LogInformation("Starting export");
			await exporter.ExportChannelAsync(request);
			_logger.LogInformation("Finished exporting");
			var stream = new FileStream(path, FileMode.Open);

			var ext = options.export_format.GetFileExtension();
			if (ext == "txt") ext = "plain";
			Response.ContentType = $"text/{ext}; charset=UTF-8";
			Response.StatusCode = 200;

			deleteFile(path);

			return stream;
		}

		async internal void deleteFile(string path)
		{
			await Task.Delay(TimeSpan.FromSeconds(30));
			var exists = System.IO.File.Exists(path);
			if (exists)
			{
				try
				{
					System.IO.File.Delete(path);
					_logger.LogInformation($"Deleted {path}");
				}
				catch { }
			}

		}

		internal Stream GenerateStreamFromString(string s)
		{
			var stream = new MemoryStream();
			var writer = new StreamWriter(stream);
			writer.Write(s);
			writer.Flush();
			stream.Position = 0;
			return stream;
		}
	}

	public class ExportOptions
	{
		public string token { get; set; }
		public string channel_id { get; set; }

		public string after { get; set; }

		public string before { get; set; }

		public ExportFormat export_format { get; set; } = ExportFormat.HtmlDark;

		public string date_format { get; set; } = "dd-MMM-yy hh:mm tt";
	}
}


<|start_filename|>ExportAPI/util/SnakeCaseNamingPolicy.cs<|end_filename|>
using System.Text.Json;
using Newtonsoft.Json.Serialization;

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
	private readonly SnakeCaseNamingStrategy _newtonsoftSnakeCaseNamingStrategy
		= new SnakeCaseNamingStrategy();

	public static SnakeCaseNamingPolicy Instance { get; } = new SnakeCaseNamingPolicy();

	public override string ConvertName(string name)
	{
		/* A conversion to snake case implementation goes here. */

		return _newtonsoftSnakeCaseNamingStrategy.GetPropertyName(name, false);
	}
}

<|start_filename|>ExportAPI/Controllers/v1/ExportController.cs<|end_filename|>
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Core.Exporting.Filtering;
using DiscordChatExporter.Core.Discord.Data;

namespace ExportAPI.Controllers
{
	[ApiController]
	[Route("v1/export")]
	public class ExportControllerv1 : ControllerBase
	{
		private readonly ILogger<ExportController> _logger;

		private readonly HttpClient _httpclient = new HttpClient();

		public ExportControllerv1(ILogger<ExportController> logger)
		{
			_logger = logger;

			if (!Directory.Exists("/tmp/exports"))
			{
				Directory.CreateDirectory("/tmp/exports");
			}

			var nowhex = DateTime.Now.Ticks.ToString("X2");
			if (!Directory.Exists($"/tmp/exports/{nowhex}"))
			{
				Directory.CreateDirectory($"/tmp/exports/{nowhex}");
			}
		}

		internal static string GetPath(string channelId)
		{
			var nowhex = DateTime.Now.Ticks.ToString("X2");
			return $"/tmp/exports/{nowhex}/{channelId}.html";
		}

		[HttpPost]
		public async Task<Stream> Post(ExportOptionsv1 options)
		{
			var parsed = Snowflake.TryParse(options.ChannelId);
			var channelId = parsed ?? Snowflake.Zero;

			var token = new AuthToken(AuthTokenKind.Bot, options.Token);
			var client = new DiscordClient(token);
			Channel channel;
			try
			{
				channel = await client.GetChannelAsync(channelId);
			}
			catch (DiscordChatExporterException e)
			{
				var isUnauthorized = e.Message.Contains("Authentication");
				var content = isUnauthorized ? "Invalid Discord token provided." : "Please provide a valid channel";

				Response.ContentType = "application/json";
				Response.StatusCode = isUnauthorized ? 401 : 409;
				return GenerateStreamFromString("{ \"error\": \"" + content + "\" }");
			}

			var guild = await client.GetGuildAsync(channel.GuildId);

			using var req = new HttpRequestMessage(HttpMethod.Get, new Uri("https://discord.com/api/v8/users/@me"));
			req.Headers.Authorization = token.GetAuthenticationHeader();
			var res = await _httpclient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
			var text = await res.Content.ReadAsStringAsync();
			var me = DiscordChatExporter.Core.Discord.Data.User.Parse(JsonDocument.Parse(text).RootElement.Clone());

			_logger.LogInformation($"[{me.FullName} ({me.Id})] Exporting #{channel.Name} ({channel.Id}) within {guild.Name} ({guild.Id})");
			var path = GetPath(channel.Id.ToString());

			var request = new ExportRequest(
				guild,
				channel,
				path,
				ExportFormat.HtmlDark,
				null,
				null,
				null,
				MessageFilter.Null,
				false,
				false,
				"dd-MMM-yy hh:mm tt"
			);

			var exporter = new ChannelExporter(client);

			await exporter.ExportChannelAsync(request);

			var stream = new FileStream(path, FileMode.Open);

			Response.ContentType = "text/html; charset=UTF-8";
			Response.StatusCode = 200;

			deleteFile(path);

			return stream;
		}

		async internal void deleteFile(string path)
		{
			await Task.Delay(TimeSpan.FromSeconds(30));
			System.IO.File.Delete(path);
			_logger.LogInformation($"Deleted {path}");
		}

		internal Stream GenerateStreamFromString(string s)
		{
			var stream = new MemoryStream();
			var writer = new StreamWriter(stream);
			writer.Write(s);
			writer.Flush();
			stream.Position = 0;
			return stream;
		}
	}

	public class ExportOptionsv1
	{
		public string Token { get; set; }
		public string ChannelId { get; set; }
	}
}
