From a5247d6f5dd61cc58a928ce15d9d921b5a299730 Mon Sep 17 00:00:00 2001 From: Andrew Glaze Date: Tue, 19 Aug 2025 22:57:10 -0400 Subject: [PATCH] init --- .gitignore | 10 ++ Package.resolved | 240 ++++++++++++++++++++++++++++++++++++++++ Package.swift | 23 ++++ Sources/nahi/nahi.swift | 91 +++++++++++++++ 4 files changed, 364 insertions(+) create mode 100644 .gitignore create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 Sources/nahi/nahi.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b976d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc + +*token.txt diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..bc070e8 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,240 @@ +{ + "originHash" : "2cbecfe2d4d67f296ed2aa3c2fbb18dd36c40a3747dfb0c13c236dff21aa978a", + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "60235983163d040f343a489f7e2e77c1918a8bd9", + "version" : "1.26.1" + } + }, + { + "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" : "801decdb5130a2053274c5507e847c33500790c5", + "version" : "1.13.2" + } + }, + { + "identity" : "geminikit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/guitaripod/GeminiKit", + "state" : { + "revision" : "63827c07138f2c1fbf34b97b877c2a22d348e88f", + "version" : "1.0.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-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", + "version" : "1.0.4" + } + }, + { + "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" : "c059d9c9d08d6654b9a92dda93d9049a278964c6", + "version" : "1.12.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "334e682869394ee239a57dbe9262bff3cd9495bd", + "version" : "3.14.0" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "1625f271afb04375bf48737a5572613248d0e7a0", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "a0a57e949a8903563aba4615869310c0ebf14c03", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "1c30f0f2053b654e3d1302492124aa6d242cdba7", + "version" : "2.86.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "a55c3dd3a81d035af8a20ce5718889c0dcab073d", + "version" : "1.29.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "5e9e99ec96c53bc2c18ddd10c1e25a3cd97c55e5", + "version" : "1.38.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "385f5bd783ffbfff46b246a7db7be8e4f04c53bd", + "version" : "2.33.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "e645014baea2ec1c2db564410c51a656cf47c923", + "version" : "1.25.1" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle", + "state" : { + "revision" : "e7187309187695115033536e8fc9b2eb87fd956d", + "version" : "2.8.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "890830fff1a577dc83134890c7984020c5f6b43b", + "version" : "1.6.2" + } + }, + { + "identity" : "swift-websocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hummingbird-project/swift-websocket.git", + "state" : { + "revision" : "7c0ea7b877386d117441877a7cb24005326750a7", + "version" : "1.3.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..aacac96 --- /dev/null +++ b/Package.swift @@ -0,0 +1,23 @@ +// 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: "nahi", + dependencies: [ + .package(url: "https://github.com/DiscordBM/DiscordBM.git", from: "1.13.2"), + .package(url: "https://github.com/guitaripod/GeminiKit", from: "1.0.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: "nahi", + dependencies: [ + .product(name: "DiscordBM", package: "DiscordBM"), + .product(name: "GeminiKit", package: "GeminiKit") + ] + ), + ] +) diff --git a/Sources/nahi/nahi.swift b/Sources/nahi/nahi.swift new file mode 100644 index 0000000..71c988d --- /dev/null +++ b/Sources/nahi/nahi.swift @@ -0,0 +1,91 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import DiscordBM +import GeminiKit + +@main +struct Nahi { + + static func main() async throws { + let bot = await BotGatewayManager( + token: try! String(contentsOfFile: "discord_token.txt", encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines), + presence: .init(activities: [.init(name: "Vibing", type: .competing)], status: .online, afk: false), + intents: [.guildMessages, .messageContent] + ) + + let gemini = GeminiKit(configuration: .init(apiKey: try! String(contentsOfFile: "gemini_token.txt", encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines))) + + await withTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + await bot.connect() + } + + taskGroup.addTask { + for await event in await bot.events { + await EventHandler(event: event, client: bot.client, gemini: gemini).handleAsync() + } + } + } + } +} + +struct EventHandler: GatewayEventHandler { + let event: Gateway.Event + let client: any DiscordClient + let gemini: GeminiKit + + func onMessageCreate(_ payload: Gateway.MessageCreate) async throws { + guard !(payload.author?.bot ?? false) else { return } + guard payload.mentions.contains(where: { mention in mention.id.rawValue == client.appId!.rawValue }) else { return } + _ = try await client.triggerTypingIndicator(channelId: payload.channel_id) + + let streamRes = await Task { + return try await gemini.streamGenerateContent(model: .gemini20Flash, prompt: payload.content) + }.result + + var message: DiscordChannel.Message? + switch (streamRes) { + case .success(let stream): + var resStr = "" + for try await chunk in stream { + switch chunk.candidates?.first?.content.parts.first.unsafelyUnwrapped { + case .text(let text): + resStr.append(text) + if resStr.count > 2000 { + resStr = text + message = nil + } + default: + print("Unknown content type") + } + if let message = message { + try await client.updateMessage( + channelId: message.channel_id, + messageId: message.id, + payload: .init(content: resStr) + ).guardSuccess() + } else { + let messageRes = try await client.createMessage( + channelId: payload.channel_id, + payload: .init(content: resStr) + ) + try messageRes.guardSuccess() + message = try messageRes.decode() + } + if chunk.candidates?.first?.finishReason == nil { + _ = try await client.triggerTypingIndicator(channelId: payload.channel_id) + } else { + let tmpMsg = try await client.createMessage(channelId: payload.channel_id, payload: .init(content: "a")).decode() + _ = try await client.deleteMessage(channelId: tmpMsg.channel_id, messageId: tmpMsg.id) + + } + } + case .failure(let err): + try await client.createMessage( + channelId: payload.channel_id, + payload: .init(content: "\(err.localizedDescription)") + ).guardSuccess() + } + } +}