Files
GLEGram-iOS/Swiftgram/SGSettingsUI/Sources/PluginCodeEditorController.swift
T
Leeksov 4647310322 GLEGram 12.5 — Initial public release
Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
2026-04-06 09:48:12 +03:00

236 lines
9.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// MARK: GLEGram Plugin code editor (create/edit JS plugins inline)
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import SGSimpleSettings
// MARK: - State
private final class PluginCodeEditorStateHolder {
var name: String
var code: String
init(name: String, code: String) {
self.name = name
self.code = code
}
}
private struct PluginCodeEditorState: Equatable {
var name: String
var code: String
}
// MARK: - Entries
private enum PluginCodeEditorEntry: ItemListNodeEntry {
case nameInput(id: Int, text: String, placeholder: String)
case codeInput(id: Int, text: String, placeholder: String)
case notice(id: Int, text: String)
var section: ItemListSectionId {
switch self {
case .nameInput: return 0
case .codeInput: return 1
case .notice: return 2
}
}
var stableId: Int {
switch self {
case .nameInput(let id, _, _): return id
case .codeInput(let id, _, _): return id
case .notice(let id, _): return id
}
}
static func == (lhs: PluginCodeEditorEntry, rhs: PluginCodeEditorEntry) -> Bool {
switch (lhs, rhs) {
case let (.nameInput(a, t1, p1), .nameInput(b, t2, p2)): return a == b && t1 == t2 && p1 == p2
case let (.codeInput(a, t1, p1), .codeInput(b, t2, p2)): return a == b && t1 == t2 && p1 == p2
case let (.notice(a, t1), .notice(b, t2)): return a == b && t1 == t2
default: return false
}
}
static func < (lhs: PluginCodeEditorEntry, rhs: PluginCodeEditorEntry) -> Bool {
lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let args = arguments as! PluginCodeEditorArguments
switch self {
case .nameInput(_, let text, let placeholder):
return ItemListSingleLineInputItem(
presentationData: presentationData,
title: NSAttributedString(),
text: text,
placeholder: placeholder,
sectionId: section,
textUpdated: { newText in args.updatedName(newText) },
action: {}
)
case .codeInput(_, let text, let placeholder):
return ItemListMultilineInputItem(
presentationData: presentationData,
text: text,
placeholder: placeholder,
maxLength: nil,
sectionId: section,
style: .blocks,
textUpdated: { newText in args.updatedCode(newText) },
updatedFocus: nil,
tag: nil,
action: nil,
inlineAction: nil
)
case .notice(_, let text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: section)
}
}
}
// MARK: - Arguments
private final class PluginCodeEditorArguments {
var updatedName: (String) -> Void = { _ in }
var updatedCode: (String) -> Void = { _ in }
}
private final class PluginCodeEditorNavActions {
var cancel: (() -> Void)?
var done: (() -> Void)?
}
// MARK: - Entries builder
private func pluginCodeEditorEntries(state: PluginCodeEditorState, presentationData: PresentationData) -> [PluginCodeEditorEntry] {
let lang = presentationData.strings.baseLanguageCode
var entries: [PluginCodeEditorEntry] = []
entries.append(.nameInput(id: 0, text: state.name, placeholder: lang == "ru" ? "Имя плагина" : "Plugin name"))
entries.append(.codeInput(id: 1, text: state.code, placeholder: lang == "ru" ? "JavaScript код..." : "JavaScript code..."))
let noticeText = lang == "ru"
? "Используйте GLEGram.ui, GLEGram.chat, GLEGram.compose, GLEGram.messageActions, GLEGram.intercept, GLEGram.network, GLEGram.settings, GLEGram.events API."
: "Use GLEGram.ui, GLEGram.chat, GLEGram.compose, GLEGram.messageActions, GLEGram.intercept, GLEGram.network, GLEGram.settings, GLEGram.events API."
entries.append(.notice(id: 2, text: noticeText))
return entries
}
// MARK: - Controller
public func pluginCodeEditorController(context: AccountContext, existingPlugin: PluginInfo?, initialCode: String, onSave: @escaping (PluginInfo) -> Void) -> ViewController {
let initialName = existingPlugin?.metadata.name ?? ""
let stateHolder = PluginCodeEditorStateHolder(name: initialName, code: initialCode)
let navActions = PluginCodeEditorNavActions()
let statePromise = ValuePromise(PluginCodeEditorState(name: initialName, code: initialCode), ignoreRepeated: true)
let arguments = PluginCodeEditorArguments()
arguments.updatedName = { newName in
stateHolder.name = newName
statePromise.set(PluginCodeEditorState(name: newName, code: stateHolder.code))
}
arguments.updatedCode = { newCode in
stateHolder.code = newCode
statePromise.set(PluginCodeEditorState(name: stateHolder.name, code: newCode))
}
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, PluginCodeEditorArguments)) in
let lang = presentationData.strings.baseLanguageCode
let title = existingPlugin != nil
? (lang == "ru" ? "Редактор" : "Editor")
: (lang == "ru" ? "Новый плагин" : "New Plugin")
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(title),
leftNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { navActions.cancel?() }),
rightNavigationButton: ItemListNavigationButton(content: .text(lang == "ru" ? "Сохранить" : "Save"), style: .bold, enabled: !state.code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, action: { navActions.done?() }),
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)
)
let entries = pluginCodeEditorEntries(state: state, presentationData: presentationData)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
ensureVisibleItemTag: nil,
initialScrollToItem: nil
)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
navActions.cancel = { [weak controller] in
controller?.dismiss()
}
navActions.done = { [weak controller] in
let code = stateHolder.code.trimmingCharacters(in: .whitespacesAndNewlines)
guard !code.isEmpty else { return }
// Parse metadata from code
var metadata: PluginMetadata
if let parsed = PluginMetadataParser.parse(content: code) {
metadata = parsed
} else {
let name = stateHolder.name.trimmingCharacters(in: .whitespacesAndNewlines)
let safeName = name.isEmpty ? "Untitled Plugin" : name
let safeId = existingPlugin?.metadata.id ?? safeName.lowercased()
.replacingOccurrences(of: " ", with: "-")
.filter { $0.isLetter || $0.isNumber || $0 == "-" }
let id = safeId.isEmpty ? "plugin-\(UUID().uuidString.prefix(8))" : safeId
metadata = PluginMetadata(id: id, name: safeName, description: "", version: "1.0", author: "")
}
// If editing, keep the same ID
if let existing = existingPlugin {
metadata = PluginMetadata(
id: existing.metadata.id,
name: metadata.name,
description: metadata.description,
version: metadata.version,
author: metadata.author,
iconRef: metadata.iconRef,
minVersion: metadata.minVersion,
hasUserDisplay: metadata.hasUserDisplay
)
}
// Write file
let fileManager = FileManager.default
guard let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return }
let pluginsDir = supportURL.appendingPathComponent("Plugins", isDirectory: true)
try? fileManager.createDirectory(at: pluginsDir, withIntermediateDirectories: true)
let destURL = pluginsDir.appendingPathComponent("\(metadata.id).js")
try? code.write(to: destURL, atomically: true, encoding: .utf8)
// Update installed list
let pluginInfo = PluginInfo(metadata: metadata, path: destURL.path, enabled: true, hasSettings: false)
var plugins: [PluginInfo]
if let data = SGSimpleSettings.shared.installedPluginsJson.data(using: .utf8),
let existing = try? JSONDecoder().decode([PluginInfo].self, from: data) {
plugins = existing
} else {
plugins = []
}
plugins.removeAll { $0.metadata.id == metadata.id }
plugins.append(pluginInfo)
if let data = try? JSONEncoder().encode(plugins),
let json = String(data: data, encoding: .utf8) {
SGSimpleSettings.shared.installedPluginsJson = json
SGSimpleSettings.shared.synchronizeShared()
}
// Reload plugins
PluginRunner.install()
onSave(pluginInfo)
controller?.dismiss()
}
return controller
}