commit 14358902185a75f02ca116060512e20c765151c5 Author: Andrew Glaze Date: Sat Feb 28 12:10:37 2026 -0500 feat: wolfram alpha diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec833f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +.env diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..582d10d --- /dev/null +++ b/Package.resolved @@ -0,0 +1,267 @@ +{ + "originHash" : "b3f560c4f196109910c6c326162b62c9d4c171d5ba1f22725daa5f5a9ffd2c75", + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "2fc4652fb4689eb24af10e55cabaa61d8ba774fd", + "version" : "1.32.0" + } + }, + { + "identity" : "compress-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/adam-fowler/compress-nio.git", + "state" : { + "revision" : "e1caa19077dda4b00441142ef57da3db02acd466", + "version" : "1.4.2" + } + }, + { + "identity" : "discordbm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DiscordBM/DiscordBM.git", + "state" : { + "revision" : "90d98d45ae3ee8ed8a2d3d6d86be4aa8ab56515c", + "version" : "1.16.0" + } + }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "3498e60218e6003894ff95192d756e238c01f44e", + "version" : "4.7.1" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "810496cf121e525d660cd0ea89a758740476b85f", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "2971dd5d9f6e0515664b01044826bcea16e59fac", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "24ccdeeeed4dfaae7955fcac9dbf5489ed4f1a25", + "version" : "1.18.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-configuration", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-configuration.git", + "state" : { + "revision" : "1bb939fe7bbb00b8f8bab664cc90020c035c08d9", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "6f70fa9eab24c1fd982af18c281c4525d05e3095", + "version" : "4.2.0" + } + }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "e109d8b5308d0e05201d9a1dd1c475446a946a11", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "bbd81b6725ae874c69e9b8c8804d462356b55523", + "version" : "1.10.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "e932d3c4d8f77433c8f7093b5ebcbf91463948a0", + "version" : "2.95.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "3df009d563dc9f21a5c85b33d8c2e34d2e4f8c3b", + "version" : "1.32.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "b6571f3db40799df5a7fc0e92c399aa71c883edd", + "version" : "1.40.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "173cc69a058623525a58ae6710e2f5727c663793", + "version" : "2.36.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "60c3e187154421171721c1a38e800b390680fb5d", + "version" : "1.26.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "d0997351b0c7779017f88e7a93bc30a1878d7f29", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle", + "state" : { + "revision" : "89888196dd79c61c50bca9a103d8114f32e1e598", + "version" : "2.10.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", + "version" : "1.6.4" + } + }, + { + "identity" : "swift-websocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hummingbird-project/swift-websocket.git", + "state" : { + "revision" : "ca48d46c25f8fa948d37eaa480c73172182cf90f", + "version" : "1.5.0" + } + }, + { + "identity" : "xmlcoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CoreOffice/XMLCoder.git", + "state" : { + "revision" : "5e1ada828d2618ecb79c974e03f79c8f4df90b71", + "version" : "0.18.0" + } + }, + { + "identity" : "zstd", + "kind" : "remoteSourceControl", + "location" : "https://github.com/facebook/zstd.git", + "state" : { + "revision" : "f8745da6ff1ad1e7bab384bd1f9d742439278e99", + "version" : "1.5.7" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..24ecdf4 --- /dev/null +++ b/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "zundamon", + dependencies: [ + .package(url: "https://github.com/DiscordBM/DiscordBM.git", from: "1.16.0"), + .package(url: "https://github.com/thebarndog/swift-dotenv.git", from: "2.1.0"), + .package(url: "https://github.com/CoreOffice/XMLCoder.git", from: "0.18.0") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "zundamon", + dependencies: [ + .product(name: "DiscordBM", package: "discordbm"), + .product(name: "SwiftDotenv", package: "swift-dotenv"), + .product(name: "XMLCoder", package: "xmlcoder") + ], + ), + ] +) diff --git a/Sources/zundamon/Mathsumoto.swift b/Sources/zundamon/Mathsumoto.swift new file mode 100644 index 0000000..9f01b3f --- /dev/null +++ b/Sources/zundamon/Mathsumoto.swift @@ -0,0 +1,46 @@ +import Foundation +import DiscordBM +import SwiftDotenv + +@main +struct Mathsumoto { + nonisolated(unsafe) static private(set) var ownID: UserSnowflake? = nil + + static func main() async throws { + let tmp = Result { try Dotenv.configure() } + switch (tmp) { + case .success: + print("Loaded .env file") + case .failure: + break + } + + let token = ProcessInfo.processInfo.environment["DISCORD_TOKEN"]! + + let bot = await BotGatewayManager(token: token, intents: [.guildMessages, .messageContent]) + + await withTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + await bot.connect() + let tmp = try! await bot.client.getOwnUser() + ownID = try! tmp.decode().id + } + + for await event in await bot.events { + taskGroup.addTask { + await EventHandler(event: event, client: bot.client).handleAsync() + } + } + } + } +} + +struct EventHandler: GatewayEventHandler { + let event: Gateway.Event + let client: any DiscordClient + + func onMessageCreate(_ payload: Gateway.MessageCreate) async throws { + try await MessageHandler(ctx: payload, client: client).handle() + } + +} diff --git a/Sources/zundamon/MessageHandler.swift b/Sources/zundamon/MessageHandler.swift new file mode 100644 index 0000000..138f576 --- /dev/null +++ b/Sources/zundamon/MessageHandler.swift @@ -0,0 +1,111 @@ +import Foundation +import FoundationNetworking +import DiscordBM + +struct MessageHandler { + let ctx: Gateway.MessageCreate + let client: any DiscordClient + + static let prefix = ":" + static let zundaGifData = try? Data(contentsOf: URL(filePath: "media/zundamone.gif")) + + func handle() async throws { + guard !(ctx.author?.bot ?? false) else { return } + if (ctx.content.hasPrefix(MessageHandler.prefix)) { + let split = ctx.content.split(separator: " ") + let command = split.first?.trimmingPrefix(MessageHandler.prefix) + let args = split[1...] + + switch command { + case "wow": try await handleWow(args) + case "domath": try await Wolfram.handleMath(args, client: client, ctx: ctx) + default: break + } + } else if ctx.mentions.contains(where: { $0.id == Mathsumoto.ownID }) { + if ctx.content + .replacingOccurrences(of: "<@\(Mathsumoto.ownID!.rawValue)>", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + .count == 0, + let zundaGif = MessageHandler.zundaGifData + { + 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() + } else { + try await handle8Ball() + } + } + } + + static let ballResponses = [ + "It is certain.", + "It is decidedly so.", + "Without a doubt.", + "Yes – definitely.", + "You may rely on it.", + "As I see it, yes.", + "Most likely.", + "Outlook good.", + "Yes.", + "Signs point to yes.", + "Reply hazy, try again.", + "Ask again later.", + "Better not tell you now.", + "Cannot predict now.", + "Concentrate and ask again.", + "Don’t count on it.", + "My reply is no.", + "My sources say no.", + "Outlook not so good.", + "Very doubtful.", + "Ui beam", + "We are Shigure Ui", + "We are Shigure Ux", + ] + func handle8Ball() async throws { + try await client.createMessage( + channelId: ctx.channel_id, + payload: .init( + content: MessageHandler.ballResponses.randomElement(), + message_reference: .init( + type: .default, + message_id: ctx.id, + channel_id: ctx.channel_id, + guild_id: ctx.guild_id, + ), + ) + ).guardSuccess() + } + + static let wows = [ + "<:wow:1477062414913634334>", + "<:wow2:1477062432357875948>", + "<:wow4:1477062471746588713>", + "<:wow5:1477062452804849845>" + ] + func handleWow(_ args: ArraySlice) async throws { + try await client.createMessage( + channelId: ctx.channel_id, + payload: .init( + content: MessageHandler.wows.randomElement(), + message_reference: .init( + type: .default, + message_id: ctx.id, + channel_id: ctx.channel_id, + guild_id: ctx.guild_id, + ), + ) + ).guardSuccess() + } + +} diff --git a/Sources/zundamon/Wolfram.swift b/Sources/zundamon/Wolfram.swift new file mode 100644 index 0000000..979eb49 --- /dev/null +++ b/Sources/zundamon/Wolfram.swift @@ -0,0 +1,105 @@ +import Foundation +import FoundationNetworking +import XMLCoder +import DiscordBM + +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(by: "\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: Gateway.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: .default, + message_id: ctx.id, + channel_id: ctx.channel_id, + guild_id: ctx.guild_id, + ), + files: files, + attachments: attachments + )).guardSuccess() + } +} + +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/media/zundamone.gif b/media/zundamone.gif new file mode 100644 index 0000000..e279a2e Binary files /dev/null and b/media/zundamone.gif differ