From 6a48936f6c00eab56a774dceb8efd2d4860f5e32 Mon Sep 17 00:00:00 2001 From: Andrew Glaze Date: Fri, 20 Mar 2026 19:13:45 -0400 Subject: [PATCH] Image attach --- Sources/DiscordKit/DiscordClient.swift | 4 +- Sources/DiscordKit/GatewayClient.swift | 2 - Sources/DiscordKit/Models.swift | 64 +++++++- Sources/zundamon/Actions.swift | 214 ++++++++++++------------- Sources/zundamon/MessageHandler.swift | 36 ++--- Sources/zundamon/Wolfram.swift | 208 ++++++++++++------------ Sources/zundamon/Zundamon.swift | 2 - 7 files changed, 289 insertions(+), 241 deletions(-) diff --git a/Sources/DiscordKit/DiscordClient.swift b/Sources/DiscordKit/DiscordClient.swift index e7eccec..018e059 100644 --- a/Sources/DiscordKit/DiscordClient.swift +++ b/Sources/DiscordKit/DiscordClient.swift @@ -7,6 +7,7 @@ public struct DiscordClient: Sendable { static let apiUrl: URL = URL(string: "https://discord.com/api/v10")! let token: String + @discardableResult private func authReq(_ request: consuming URLRequest) async throws -> (Data, URLResponse) { request.setValue("Bot \(token)", forHTTPHeaderField: "Authorization") request.setValue("DiscordKit (https:\\candy123.moe, v0.0.1)", forHTTPHeaderField: "User-Agent") @@ -44,8 +45,7 @@ public struct DiscordClient: Sendable { let json = JSONEncoder() req.httpBody = try json.encode(payload) - let (data, res) = try await authReq(req) - + try await authReq(req) } } diff --git a/Sources/DiscordKit/GatewayClient.swift b/Sources/DiscordKit/GatewayClient.swift index 0354aa9..f09ee71 100644 --- a/Sources/DiscordKit/GatewayClient.swift +++ b/Sources/DiscordKit/GatewayClient.swift @@ -32,8 +32,6 @@ actor GatewayClient { } try await sendIdentify(intents: intents) - //dump(try await getMessage()) - //print("got here") _ = await heartbeatTask.result } diff --git a/Sources/DiscordKit/Models.swift b/Sources/DiscordKit/Models.swift index 08e551b..238e600 100644 --- a/Sources/DiscordKit/Models.swift +++ b/Sources/DiscordKit/Models.swift @@ -96,13 +96,57 @@ public struct GatewayHello: Codable, Sendable { } public struct CreateMessageReq: Codable, Sendable { - public init(content: String? = nil, message_reference: MessageRefrence? = nil) { + public init(content: String? = nil, message_reference: MessageRefrence? = nil, embeds: [Embed]? = nil, files: [RawFile]? = nil, attachments: [Attachment]? = nil) { self.content = content self.message_reference = message_reference + self.embeds = embeds + self.files = files + self.attachments = attachments } public let content: String? public let message_reference: MessageRefrence? + public let embeds: [Embed]? + public let files: [RawFile]? + public let attachments: [Attachment]? +} + +public struct Attachment: Codable, Sendable { + public init(index: Int, filename: String? = nil) { + self.index = index + self.filename = filename + } + + public let index: Int + public let filename: String? +} + +public struct RawFile: Codable, Sendable { + public init(data: Data, filename: String? = nil) { + self.data = data + self.filename = filename + } + + public let data: Data + public let filename: String? +} + +public struct Embed: Codable, Sendable { + public init(description: String? = nil, image: EmbedImage? = nil) { + self.description = description + self.image = image + } + + public let description: String? + public let image: EmbedImage? +} + +public struct EmbedImage: Codable, Sendable { + public init(url: String) { + self.url = url + } + + public let url: String } public struct MessageRefrence: Codable, Sendable { @@ -125,12 +169,28 @@ public struct MessageCreate: Codable, Sendable { public let guild_id: String? public let author: User? public let content: String - public let mentions: [User] + public let mentions: [MentionUser] + public let member: GuildMember? } public struct User: Codable, Sendable { public let id: String? public let bot: Bool? + public let global_name: String? + public let username: String +} + +public struct GuildMember: Codable, Sendable { + public let user: User? + public let nick: String? +} + +public struct MentionUser: Codable, Sendable { + public let id: String? + public let bot: Bool? + public let global_name: String? + public let username: String + public let member: GuildMember? } diff --git a/Sources/zundamon/Actions.swift b/Sources/zundamon/Actions.swift index 1a09e17..5833a22 100644 --- a/Sources/zundamon/Actions.swift +++ b/Sources/zundamon/Actions.swift @@ -1,107 +1,107 @@ -// import DiscordKit -// -// struct Actions { -// static func getUserFromMention(_ mention: MentionUser) -> String { -// mention.member?.nick ?? mention.global_name ?? mention.username -// } -// -// static func performAction(ctx: MessageCreate, client: DiscordClient, resOpts: [String]) async throws { -// let author = "**\(ctx.member?.nick ?? ctx.author?.global_name ?? ctx.author?.username ?? "Zundamon")**" -// let dests = ctx.mentions.map(getUserFromMention).map({ "**\($0)**" }) -// let orig: String -// let dest: String -// if let firstDest = dests.first { -// orig = "\(author)" -// dest = "\(firstDest)" -// } else { -// orig = "**Zundamon**" -// dest = "\(author)" -// } -// -// guard let res = resOpts.randomElement()? -// .replacingOccurrences(of: "{orig}", with: orig) -// .replacingOccurrences(of: "{dest}", with: dest) -// else { print("retOptions empty"); return } -// -// let retMsg = res -// -// try await client.createMessage( -// channelId: ctx.channel_id, -// payload: .init( -// embeds: [.init(description: retMsg)], -// ) -// ).guardSuccess() -// } -// -// static let hugRes = (try? String(contentsOfFile: "resources/choices/hug.txt", encoding: .utf8))? -// .split(separator: "\n") -// .map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) -// -// static func hug( -// _ args: ArraySlice, -// client: DiscordClient, -// ctx: Gateway.MessageCreate -// ) async throws { -// guard let hugRes = hugRes else { print("hug.txt not loaded"); return } -// let author = "**\(ctx.member?.nick ?? ctx.author?.global_name ?? ctx.author?.username ?? "Zundamon")**" -// let dests = ctx.mentions.map(getUserFromMention).map({ "**\($0)**" }) -// -// let retMsg: String -// -// if dests.count > 1 { -// let groupHugs = [ -// "{subjects} all huddled together.", -// "{subjects} hugged each other pairwise, generating a total of **{total}** hugs.", -// "{subjects} hugged each other at the same time in the same place (although I'm not sure how that works with the current understanding of spacetime).", -// ] -// -// let group = [author] + dests -// let total = String(group.count) -// let subjects = String(group.joined(by: ", ")) -// -// guard let res = groupHugs.randomElement()? -// .replacingOccurrences(of: "{subjects}", with: subjects) -// .replacingOccurrences(of: "{total}", with: total) -// else { print("groupHugs.randomElement() returned null"); return } -// -// retMsg = res -// } else { -// let orig: String -// let dest: String -// if let firstDest = dests.first { -// orig = "\(author)" -// dest = "\(firstDest)" -// } else { -// orig = "**Zundamon**" -// dest = "\(author)" -// } -// -// guard let res = hugRes.randomElement()? -// .replacingOccurrences(of: "{orig}", with: orig) -// .replacingOccurrences(of: "{dest}", with: dest) -// else { print("hug.txt empty"); return } -// -// retMsg = res -// } -// -// try await client.createMessage( -// channelId: ctx.channel_id, -// payload: .init( -// embeds: [.init(description: retMsg)], -// ) -// ).guardSuccess() -// } -// -// static let patRes = (try? String(contentsOfFile: "resources/choices/pat.txt", encoding: .utf8))? -// .split(separator: "\n") -// .map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) -// -// static func pat( -// _ args: ArraySlice, -// client: DiscordClient, -// ctx: MessageCreate -// ) async throws { -// guard let patRes = patRes else { print("pat.txt not loaded"); return } -// try await performAction(ctx: ctx, client: client, resOpts: patRes) -// } -// } +import DiscordKit + +struct Actions { + static func getUserFromMention(_ mention: MentionUser) -> String { + mention.member?.nick ?? mention.global_name ?? mention.username + } + + static func performAction(ctx: MessageCreate, client: DiscordClient, resOpts: [String]) async throws { + let author = "**\(ctx.member?.nick ?? ctx.author?.global_name ?? ctx.author?.username ?? "Zundamon")**" + let dests = ctx.mentions.map(getUserFromMention).map({ "**\($0)**" }) + let orig: String + let dest: String + if let firstDest = dests.first { + orig = "\(author)" + dest = "\(firstDest)" + } else { + orig = "**Zundamon**" + dest = "\(author)" + } + + guard let res = resOpts.randomElement()? + .replacingOccurrences(of: "{orig}", with: orig) + .replacingOccurrences(of: "{dest}", with: dest) + else { print("retOptions empty"); return } + + let retMsg = res + + try await client.createMessage( + channelId: ctx.channel_id, + payload: .init( + embeds: [.init(description: retMsg)], + ) + ) + } + + static let hugRes = (try? String(contentsOfFile: "resources/choices/hug.txt", encoding: .utf8))? + .split(separator: "\n") + .map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) + + static func hug( + _ args: ArraySlice, + client: DiscordClient, + ctx: MessageCreate + ) async throws { + guard let hugRes = hugRes else { print("hug.txt not loaded"); return } + let author = "**\(ctx.member?.nick ?? ctx.author?.global_name ?? ctx.author?.username ?? "Zundamon")**" + let dests = ctx.mentions.map(getUserFromMention).map({ "**\($0)**" }) + + let retMsg: String + + if dests.count > 1 { + let groupHugs = [ + "{subjects} all huddled together.", + "{subjects} hugged each other pairwise, generating a total of **{total}** hugs.", + "{subjects} hugged each other at the same time in the same place (although I'm not sure how that works with the current understanding of spacetime).", + ] + + let group = [author] + dests + let total = String(group.count) + let subjects = String(group.joined(separator: ", ")) + + guard let res = groupHugs.randomElement()? + .replacingOccurrences(of: "{subjects}", with: subjects) + .replacingOccurrences(of: "{total}", with: total) + else { print("groupHugs.randomElement() returned null"); return } + + retMsg = res + } else { + let orig: String + let dest: String + if let firstDest = dests.first { + orig = "\(author)" + dest = "\(firstDest)" + } else { + orig = "**Zundamon**" + dest = "\(author)" + } + + guard let res = hugRes.randomElement()? + .replacingOccurrences(of: "{orig}", with: orig) + .replacingOccurrences(of: "{dest}", with: dest) + else { print("hug.txt empty"); return } + + retMsg = res + } + + try await client.createMessage( + channelId: ctx.channel_id, + payload: .init( + embeds: [.init(description: retMsg)], + ) + ) + } + + static let patRes = (try? String(contentsOfFile: "resources/choices/pat.txt", encoding: .utf8))? + .split(separator: "\n") + .map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) + + static func pat( + _ args: ArraySlice, + client: DiscordClient, + ctx: MessageCreate + ) async throws { + guard let patRes = patRes else { print("pat.txt not loaded"); return } + try await performAction(ctx: ctx, client: client, resOpts: patRes) + } +} diff --git a/Sources/zundamon/MessageHandler.swift b/Sources/zundamon/MessageHandler.swift index 1f3a54c..594f0e8 100644 --- a/Sources/zundamon/MessageHandler.swift +++ b/Sources/zundamon/MessageHandler.swift @@ -20,32 +20,30 @@ struct MessageHandler { switch command { case "wow": try await handleWow(args) - // case "domath": try await Wolfram.handleMath(args, client: client, ctx: ctx) - // case "hug": try await Actions.hug(args, client: client, ctx: ctx) - // case "pat": try await Actions.pat(args, client: client, ctx: ctx) - // case "pet": try await Actions.pat(args, client: client, ctx: ctx) + case "domath": try await Wolfram.handleMath(args, client: client, ctx: ctx) + case "hug": try await Actions.hug(args, client: client, ctx: ctx) + case "pat": try await Actions.pat(args, client: client, ctx: ctx) + case "pet": try await Actions.pat(args, client: client, ctx: ctx) default: break } } else if ctx.mentions.contains(where: { $0.id == Zundamon.ownID }) { if ctx.content .replacingOccurrences(of: "<@\(Zundamon.ownID!)>", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) - .count == 0, - let zundaGif = MessageHandler.zundaGifData + .count == 0 { - // try await client.createMessage( - // channelId: ctx.channel_id, - // payload: .init( - // message_reference: .init( - // type: .default, - // message_id: ctx.id, - // channel_id: ctx.channel_id, - // guild_id: ctx.guild_id, - // ), - // files: [.init(data: .init(data: zundaGif), filename: "zundamone.gif")], - // attachments: [.init(index: 0, filename: "zundamone.gif")], - // ) - // ).guardSuccess() + try await client.createMessage( + channelId: ctx.channel_id, + payload: .init( + message_reference: .init( + type: 0, + message_id: ctx.id, + channel_id: ctx.channel_id, + guild_id: ctx.guild_id, + ), + embeds: [.init(image: .init(url: "https://candy123.moe/tetrio/zundamone.gif"))], + ) + ) } else { try await handle8Ball() } diff --git a/Sources/zundamon/Wolfram.swift b/Sources/zundamon/Wolfram.swift index 0b12cbf..9d5aecc 100644 --- a/Sources/zundamon/Wolfram.swift +++ b/Sources/zundamon/Wolfram.swift @@ -1,107 +1,101 @@ -// import Foundation -// #if canImport(FoundationNetworking) -// import FoundationNetworking -// #endif -// import DiscordKit -// import XMLCoder -// -// struct Wolfram { -// static let token = ProcessInfo.processInfo.environment["WOLFRAM_APP_ID"]! -// static let apiUrl = "http://api.wolframalpha.com/v2/query" -// -// static func getWolfram(_ question: String) async throws -> (String, URL?) { -// let encQuestion = question.addingPercentEncoding(withAllowedCharacters: .alphanumerics) -// let url = URL(string: "\(apiUrl)?appid=\(token)&input=\(encQuestion!)")! -// let (data, _) = try await URLSession.shared.data(from: url) -// -// let wolframRes = try XMLDecoder().decode(WolframQueryResult.self, from: data) -// -// let resultPod = wolframRes.pod.first(where: { $0.primary ?? false || $0.id == "Result" }) -// -// var ans: String? -// var img: URL? -// -// if let resultPod = resultPod { -// ans = String(resultPod.subpod.compactMap(\.plaintext).joined(separator: "\n")) -// .replacingOccurrences(of: " | ", with: ": ") -// } -// if let imgPod = wolframRes.pod.first(where: { -// ["RootPlot", "NumberLine", "Plot", "ImplicitPlot", "3DPlot"].contains($0.id) || -// ($0.id == "Example" && $0.scanner == "Dice") -// }) { -// ans = ans ?? "Plot:" -// img = imgPod.subpod.first?.img.src -// } -// -// if ans == nil, -// let maybePod = wolframRes.pod.first(where: { $0.title == "Input interpretation" }), -// let maybeText = maybePod.subpod.first?.plaintext -// .replacingOccurrences(of: " | ", with: ": ") { -// ans = "<:smol_rise:852763040452575252> I don't know. Maybe you meant '\(maybeText)'" -// } -// -// return (ans ?? "<:smol_rise:852763040452575252> sorry, I have no idea (´._.`)", img) -// } -// -// static func handleMath( -// _ args: ArraySlice, -// client: DiscordClient, -// ctx: MessageCreate -// ) async throws { -// try await client.triggerTypingIndicator(channelId: ctx.channel_id).guardSuccess() -// -// let question = String(args.joined(by: " ")) -// -// let (answer, img) = try await getWolfram(question) -// -// var attachments: [Payloads.Attachment] = [] -// var files: [RawFile] = [] -// -// if let img = img { -// let data = try? await URLSession.shared.data(from: img) -// if let data = data?.0 { -// attachments.append(.init(index: 0, filename: "img.gif")) -// files.append(.init(data: .init(data: data), filename: "img.gif")) -// } -// } -// -// try await client.createMessage(channelId: ctx.channel_id, payload: .init( -// content: answer, -// message_reference: .init( -// type: 0, -// message_id: ctx.id, -// channel_id: ctx.channel_id, -// guild_id: ctx.guild_id, -// ), -// files: files, -// attachments: attachments -// )) -// } -// } -// -// struct WolframQueryResult: Codable { -// struct Pod: Codable { -// let title: String -// let id: String -// let scanner: String -// let primary: Bool? -// -// let subpod: [Subpod] -// } -// -// struct Subpod: Codable { -// let title: String -// let img: Image -// let plaintext: String -// } -// -// struct Image: Codable { -// let src: URL -// } -// -// let success: Bool -// let numpods: Int -// -// let pod: [Pod] -// } -// +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import DiscordKit +import XMLCoder + +struct Wolfram { + static let token = ProcessInfo.processInfo.environment["WOLFRAM_APP_ID"]! + static let apiUrl = "http://api.wolframalpha.com/v2/query" + + static func getWolfram(_ question: String) async throws -> (String, URL?) { + let encQuestion = question.addingPercentEncoding(withAllowedCharacters: .alphanumerics) + let url = URL(string: "\(apiUrl)?appid=\(token)&input=\(encQuestion!)")! + let (data, _) = try await URLSession.shared.data(from: url) + + let wolframRes = try XMLDecoder().decode(WolframQueryResult.self, from: data) + + let resultPod = wolframRes.pod.first(where: { $0.primary ?? false || $0.id == "Result" }) + + var ans: String? + var img: URL? + + if let resultPod = resultPod { + ans = String(resultPod.subpod.compactMap(\.plaintext).joined(separator: "\n")) + .replacingOccurrences(of: " | ", with: ": ") + } + if let imgPod = wolframRes.pod.first(where: { + ["RootPlot", "NumberLine", "Plot", "ImplicitPlot", "3DPlot"].contains($0.id) || + ($0.id == "Example" && $0.scanner == "Dice") + }) { + ans = ans ?? "Plot:" + img = imgPod.subpod.first?.img.src + } + + if ans == nil, + let maybePod = wolframRes.pod.first(where: { $0.title == "Input interpretation" }), + let maybeText = maybePod.subpod.first?.plaintext + .replacingOccurrences(of: " | ", with: ": ") { + ans = "<:smol_rise:852763040452575252> I don't know. Maybe you meant '\(maybeText)'" + } + + return (ans ?? "<:smol_rise:852763040452575252> sorry, I have no idea (´._.`)", img) + } + + static func handleMath( + _ args: ArraySlice, + client: DiscordClient, + ctx: MessageCreate + ) async throws { + //try await client.triggerTypingIndicator(channelId: ctx.channel_id).guardSuccess() + + let question = String(args.joined(separator: " ")) + + let (answer, img) = try await getWolfram(question) + + var attachments: [Embed] = [] + + if let img = img { + attachments.append(.init(image: .init(url: img.absoluteString))) + } + + try await client.createMessage(channelId: ctx.channel_id, payload: .init( + content: answer, + message_reference: .init( + type: 0, + message_id: ctx.id, + channel_id: ctx.channel_id, + guild_id: ctx.guild_id, + ), + embeds: attachments + )) + } +} + +struct WolframQueryResult: Codable { + struct Pod: Codable { + let title: String + let id: String + let scanner: String + let primary: Bool? + + let subpod: [Subpod] + } + + struct Subpod: Codable { + let title: String + let img: Image + let plaintext: String + } + + struct Image: Codable { + let src: URL + } + + let success: Bool + let numpods: Int + + let pod: [Pod] +} + diff --git a/Sources/zundamon/Zundamon.swift b/Sources/zundamon/Zundamon.swift index af3ef40..245704f 100644 --- a/Sources/zundamon/Zundamon.swift +++ b/Sources/zundamon/Zundamon.swift @@ -21,8 +21,6 @@ struct Zundamon { ownID = try await bot.client.getOwnUser().id guard ownID != nil else { fatalError("Failed to get own User ID") } - // let bot = await BotGatewayManager(token: token, intents: [.guildMessages, .messageContent]) - await withThrowingTaskGroup(of: Void.self) { taskGroup in taskGroup.addTask { try await bot.connect()