diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1e77b2f21 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# Adium, revival fork + +Adium is a multi-protocol chat client for macOS, written in Objective-C on top +of libpurple. This fork brings it to macOS 13 and arm64 and adds the services +people use now. The README says what it does and how to build it. This file +says where things are and what a change has to satisfy. Four folders carry a +CLAUDE.md of their own that says how that part is put together; it loads when a +file there is opened, so read it before changing anything in that folder. + +## Where things are + +Source/ is the application: the `adium` singleton with one controller per +domain, the contact list window, the preferences window, OTR, voice notes, +logging, and most of the older features, which are small classes called +*Plugin compiled into the application. See Source/CLAUDE.md. + +Frameworks/Adium is Adium.framework: the model (AIAccount, AIListContact, +AIChat, AIContentMessage and their relatives), the protocol each controller +implements, the view the contact list is drawn with, and the loader for +.AdiumPlugin bundles. See Frameworks/Adium/CLAUDE.md. Frameworks/AIUtilities +is Cocoa categories and generic views that know nothing about chat; its +xcconfigs/ folder holds the build settings for every target. + +Plugins/ is the rest of the features, one folder each, compiled into the +application and named in Source/AICoreComponentLoader.m: the message windows +and their tabs (Dual Window Interface), the conversation rendered in a +WKWebView (WebKit Message View), the padlock and encryption menu (Secure +Messaging), JavaScript xtras, Typst formulas, emoticons, the status menu +item and the contact alerts. Plugins/Purple Service is different: it builds +the AdiumLibpurple framework, the bridge to libpurple, and every protocol +lives in it or behind it. See Plugins/Purple Service/CLAUDE.md. PurplePlugins/ +holds the prebuilt third-party protocol plug-ins that bridge loads. + +Frameworks/ also carries the prebuilt arm64 libraries the application links, +and Dependencies/ is what fetches and builds them. See Dependencies/CLAUDE.md. + +Resources/ is nibs, images, default plists, Adium.sdef, the .lproj folders +and the bundled xtras (Message Styles, Emoticons, Sounds, Dock Icons, Status +Icons, Service Icons, Menu Bar Icons, Contact List, Scripts). What the user +installs, with accounts and logs, is under ~/Library/Application Support/Adium +2.0. UnitTests/ is the XCTest target. Testing/ is harnesses run by hand and +Utilities/ is developer tools; both READMEs are generated from the scripts' +headers. Other/XtrasCreator is the companion application for making xtras, +and AdiumHelp/ is the help book. + +## Building and checking + +Build with ./install.sh --build-only, which is what continuous integration +runs: it fetches what the repository does not carry, builds, checks that every +protocol plug-in registers with the bundled libpurple, and verifies the +application's signature. Xcode alone skips the fetch and fails at a header it +cannot find. The Makefile and Release/ are leftovers of the upstream project. + +No scheme runs the unit tests. They build and run by hand: + + xcodebuild -project Adium.xcodeproj -target "Unit tests" -configuration Release \ + -destination "platform=macOS,arch=arm64" SYMROOT="$PWD/build" \ + OBJROOT="$PWD/build/Intermediates" CODE_SIGN_IDENTITY=- \ + CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM= build + DYLD_FRAMEWORK_PATH="$PWD/build/Release" xcrun xctest "build/Release/Unit tests.xctest" + +Continuous integration builds, verifies the plug-ins and the licences, and +runs nothing else. Files join the project by editing +Adium.xcodeproj/project.pbxproj; there is no workspace, package or Podfile. +/Applications/Adium.app is often a symbolic link into build/Debug, so a +script that builds for its own purposes builds somewhere else. + +## What a change has to satisfy + +The code is Objective-C under ARC. No Swift, CocoaPods or Swift Package +Manager; the deployment target and architecture are set once in +Base.xcconfig; nothing the application needs at runtime comes from Homebrew. + +A new library follows the README's "Adding a library" and Dependencies/CLAUDE.md: +a script that produces it from a pinned source, a row in the README's table +and its licence in the bundle, all in the same change. + +Every script opens with a header saying what it is for and how it is run; +Utilities/script-index.py rewrites the two generated READMEs from those +headers, so a new or renamed script means running it again. + +User-visible strings go through the string tables. Utilities/git-hooks/ +pre-commit checks them; enable it once per clone with +`git config core.hooksPath Utilities/git-hooks`. + +docs/ is ignored by the owner's decision: it holds internal working notes, +and commit c034def91 says why. Do not un-ignore it or commit anything in it. + +A commit's title is a full sentence saying what changed; its body says why, +and what was checked and how. Comments say why, not what. Commits written +with Claude end with a Co-Authored-By trailer naming the model. diff --git a/Dependencies/CLAUDE.md b/Dependencies/CLAUDE.md new file mode 100644 index 000000000..59d940278 --- /dev/null +++ b/Dependencies/CLAUDE.md @@ -0,0 +1,49 @@ +# Dependencies/ + +What Adium links against beyond macOS, and how it got there. Three kinds of +thing, and the distinction is the whole point of this folder. + +Checked in: the prebuilt arm64 frameworks and dylibs under Frameworks/ and +the protocol plug-ins under PurplePlugins/. A fresh clone builds without ever +running anything here. + +Fetched, by fetch.sh, which install.sh runs before every build: the +MMTabBarView submodule; webrtc/fetch-webrtc.sh, a prebuilt WebRTC.xcframework +checked against a pinned SHA-256 and kept out of git for its size; and +picomemo/fetch-picomemo.sh, which fetches and builds the cryptographic half +of OMEMO from a pinned commit. All three are idempotent and quiet when there +is nothing to do. + +Built, by build.sh, from Dependencies/ and only when a library has to move: +it sources phases/ in order (utility.sh for fetching and checksums, +build_dependencies.sh for the base libraries, then libffi, the image codecs, +OpenSSL, the OTR trio of gpg-error, gcrypt and libotr, libpurple, and +make_frameworks.sh, which turns the dylibs into frameworks through +framework_maker/frameworkize.py with the *-Info.plist files here). +build_other.sh and build_vv_dependencies.sh are not sourced; they are +upstream leftovers. Every source is fetched from a pinned address and checked +against a recorded SHA-256 before it unpacks, and the version each library is +stamped with must match Base.xcconfig. copy_frameworks.sh puts the output +where the project looks for it. opus/build-opus.sh builds the voice note +codec separately; rebuild-whatsmeow.sh rebuilds the WhatsApp plug-in; +refresh-libpurple-framework.sh swaps a freshly built libpurple into the +framework the application loads. patches/ holds every local change on top of +a library's release, one folder per library, with a README where the change +needs explaining. The build needs autoconf, automake, libtool, pkg-config, +meson, cmake and ninja from Homebrew and takes the better part of an hour. + +build/, source/ and Frameworks/ inside this folder are build material and +ignored; source/ runs to gigabytes and some scripts here expect it, so do not +clear it casually. + +## Adding or moving a library + +The rule is in the README under "Adding a library", and the test is one line: +every path under Frameworks/ or Dependencies/build named in an xcconfig must +be produced by a script here, and listed in the README's table of bundled +binaries or among the three fetched things. The script, the binary and the +table row land in the same change. Every shipped binary also carries its +licence in the bundle: Utilities/collect-licenses.sh gathers them from the +source trees or the pinned release URL, and Utilities/verify-licenses.sh +checks the built application, which continuous integration runs. A library +added without its licence fails there. diff --git a/Frameworks/Adium/CLAUDE.md b/Frameworks/Adium/CLAUDE.md new file mode 100644 index 000000000..90448db7d --- /dev/null +++ b/Frameworks/Adium/CLAUDE.md @@ -0,0 +1,64 @@ +# Adium.framework + +The model, the protocols the application's controllers implement, and the +views the contact list is drawn with. Everything else links it: Source/, +every folder under Plugins/, and the AdiumLibpurple framework. Nothing here +talks to a network, and nothing here is a controller; a protocol lives in +Plugins/Purple Service and a controller in Source/. + +## The model + +AIService is a protocol and AIAccount an account on it. AIAbstractAccount +holds the mechanics of connecting, reconnecting and storing settings; +AIAccount is the part a protocol's subclass overrides. AIAccountPlan describes +an account's settings form as a list of fields, each saying once where its +value lives, and AIAccountPlanFormBuilder lays that out on an AISettingsFormView. + +AIListObject is anything that can appear in the contact list. AIListContact +is one person on one account; AIMetaContact is several of those seen as one +person; AIListGroup is a group; AIListBookmark is a saved group chat; +AIContactList is the root. AIProxyListObject stands for one object in one +place in the outline view, which is how a contact appears in two groups. +AIContactObserverManager is how a change to a list object reaches everything +that displays it. + +AIChat is a conversation, with one contact or a room. What passes through it +is an AIContentObject: AIContentMessage for a message, AIContentContext for +one restored from history, and AIContentEvent, AIContentNotification, +AIContentStatus, AIContentTopic and AIContentTyping for the rest. + +AIStatus is a presence state, AIStatusGroup a folder of them, AIStatusItem +their common parent; AIStatusDefines has the keys. + +AIXtraInfo is an installed xtra of any kind; AIEmoticonPack and AISoundSet are +two kinds read into objects. AIServiceIcons, AIStatusIcons and AIUserIcons +hand out images, the last from the *UserIconSource classes. + +## What a feature implements + +AIPlugin is the base class of every component; installPlugin and +uninstallPlugin are the whole contract. A preference pane is an +AIPreferencePane, laid out on an AISettingsFormView, the fork's System +Settings-style form, with AISettingsNavigationController behind the sidebar. +An action a contact alert can take is an AIActionDetailsPane. + +Each controller in Source/ implements one of the AI*ControllerProtocol.h +files here, and AIAdiumProtocol declares `adium`, the object that owns them +all (AISharedAdium.h makes it a global). A component reaches a controller only +through those protocols. + +AICorePluginLoader loads .AdiumPlugin bundles from the application's +Contents/PlugIns and the user's PlugIns folder; the fork's JavaScript xtras +are that kind. The loader for compiled-in components is in Source/. + +## Views and helpers + +The contact list view is AIAbstractListController with AIListOutlineView +(drawing in AIListOutlineView+Drawing) and the AIListCell, AIListContactCell, +AIListGroupCell and AIListRowView classes; the window controllers that hold +one are in Source/. AIAccountMenu, AIContactMenu and AIStatusMenu build the +menus the application shows in several places. AIHTMLDecoder converts between +HTML and attributed strings and is on the path of every message in and out. +AIAddressBookController and AIAddressBookPerson are the Contacts framework +integration. AILocalizationAssistance.h has the AILocalizedString macros, and +the AILocalization* controls size themselves to a translated title. diff --git a/Plugins/Purple Service/CLAUDE.md b/Plugins/Purple Service/CLAUDE.md new file mode 100644 index 000000000..82f130f3d --- /dev/null +++ b/Plugins/Purple Service/CLAUDE.md @@ -0,0 +1,77 @@ +# Plugins/Purple Service, the bridge to libpurple + +This folder builds the AdiumLibpurple framework (target AdiumLibpurple, +Info.plist in Plists/AdiumLibpurple). Everything that speaks a protocol goes +through here: libpurple's own protocols, the prebuilt third-party plug-ins in +PurplePlugins/, and the fork's XMPP work written against the XEPs. + +## The bridge + +SLPurpleCocoaAdapter owns libpurple's core: it brings the core up when the +plug-in loads (it used to wait for the first connection), runs libpurple's +event loop on the main run loop (adiumPurpleEventloop.m) and dispatches into +it. The adiumPurple*.m files are libpurple's ui-ops, one file per subsystem, +mapping C callbacks onto the controllers: accounts, blist (the buddy list), +connection, conversation, core, dns, ft (file transfer), notify, privacy, +request (libpurple's dialogs, made native), roomlist, signals, and +certificate trust, which verifies through SecTrust and asks the user. The +fork added files beside them for what libpurple's jabber plug-in does not do: +bookmarks (XEP-0402), carbons (XEP-0280), CSI (XEP-0352), MUC self-ping +(XEP-0410), fallback indication (XEP-0428), Jingle, OMEMO and WhatsApp. +libpurple_extensions/ssl-cdsa.c is the SSL plug-in on Apple's Security +framework that every connection uses. + +CBPurpleAccount is the account base: an AIAccount whose connection is a +PurpleAccount. Its ForSubclasses category is what a protocol overrides. + +## Services + +A service is registered one of two ways, both in CBPurpleServicePlugin's +installPlugin. First by class: Jabber, Gadu-Gadu, WhatsApp, Novell and SIMPLE, each a PurpleService or AIService +subclass with a CBPurpleAccount subclass beside it (ESJabberService with +ESPurpleJabberAccount, AIWhatsAppService with AIPurpleWhatsAppAccount, and so +on). Then by descriptor: AIPurpleGenericService reads +AIPurpleServiceDescriptors.plist and registers one service for every protocol +libpurple loaded that the class list does not already answer for, each driven +by AIPurpleGenericAccount with no code of its own. That covers Bonjour, +Telegram, IRCv3, Signal, Teams (work and personal) and Matrix. The ESIRCService class remains in the folder but is not registered: IRC is served by the ircv3 plug-in through its descriptor, and the comment at that spot says how to bring the class back. + +A descriptor row carries ServiceID, ServiceClass, ServiceCodeUniqueID, the +names and descriptions, IconBaseName and ServiceImportance, and optionally +UserNameLabel, CaseSensitive, AllowedLength, AllowedCharacters, +ForbiddenCharacters, AccountNameFromUID, AccountNameIncludesHost, +UserNamesAreEmailAddresses, UserNamesArePhoneNumbers, NetworkReachability, +EchoesOutgoingMessages and Experimental. Experimental keeps the service out +of every menu until the user runs + + defaults write com.adiumX.adiumX AIShowExperimentalServices -bool YES + +and hides the offer, not accounts already made (see -isHidden in +AIPurpleGenericService). So a protocol that arrives as a libpurple plug-in is +a .so in PurplePlugins/ plus a row, and nothing else, unless it needs +behaviour of its own, as WhatsApp did. + +## XMPP + +ESPurpleJabberAccount is where the fork's XMPP work hangs, and +ESPurpleJabberAccountPlan is its settings form. The AMPurpleJabber* classes +ride on libpurple's jabber plug-in: ad hoc commands, external service +discovery (XEP-0215, where STUN and TURN come from), HTTP upload (XEP-0363, +used for pictures and voice notes), MAM (XEP-0313), service discovery +browsing and data forms among them. + +OMEMO (XEP-0384): AIOMEMOController per account; AIOMEMOStore for identity, +sessions and skipped keys on disk; AIOMEMOMessage and AIOMEMOStanza for the +wire form; AIOMEMOMedia for encrypted files (XEP-0454); and +adiumPurpleOMEMO.m where libpurple hands messages over. The cryptography is +picomemo, which Dependencies/fetch.sh builds. Testing/omemo checks each piece +against published vectors. + +Calls: AIJingleEngine does SDP and the Jingle transport; AIJingleSessionMachine +is the session state; AIJingleCallManager holds the calls and the ringing +(XEP-0353); AIJingleCallController runs one call on WebRTC; AIJingleCallUI, +AIJingleCallWindowController and AIJingleVideoView are the window; +AIJingleCallDiagnostics is "Check Call Readiness"; adiumPurpleJingle.m is the +ringing messages. Testing/webrtc has the harnesses. + +Resources/ here is the plug-in's own strings, one .lproj per language. diff --git a/Source/CLAUDE.md b/Source/CLAUDE.md new file mode 100644 index 000000000..cd6b2a722 --- /dev/null +++ b/Source/CLAUDE.md @@ -0,0 +1,70 @@ +# Source/, the application + +The Adium target. The `adium` singleton, its controllers, the contact list +and preferences windows, and some seventy small features compiled in. + +## Launch + +main.m starts AIAdium, the application delegate. applicationDidFinishLaunching +creates the AILoginController, which picks the user profile; completeLogin +then allocates the controllers in this order: preference, toolbar, debug, +contact alerts, sound, emoticon, account, contact, chat, content, dock, file +transfer, applescriptability, status (menu and interface come from the nib). +The preference and debug controllers finish loading first; then +AICoreComponentLoader instantiates every component in its list and calls +installPlugin on each; then AICorePluginLoader loads bundles; then the rest +of the controllers get controllerDidLoad, account before contact before +interface, because accounts must exist before contacts and contacts before +the list that shows them. A component may therefore use preferences and +debug logging in installPlugin and nothing else until controllerDidLoad. + +## Controllers + +Each owns one domain and implements the matching protocol in the framework. +AIAccountController: accounts, services and passwords (its categories are the +Adium*.m files: AdiumAccounts, AdiumServices, AdiumPasswords, +AdiumPreferredAccounts). AIContactController: every list object, grouping, +metacontacts and sorting (AIAlphabeticalSort, AIManualSort, ESStatusSort). +AIChatController: open chats. AIContentController: messages in and out, with +filtering, formatting, typing and events in AdiumContentFiltering, +AdiumFormatting, AdiumTyping, AdiumMessageEvents and AdiumChatEvents. +AIInterfaceController: windows, the message view and the contact list. +AIStatusController: presence, with AIAutomaticStatus for idle and away. +AIPreferenceController: defaults and panes, with AIPreferenceContainer for +per-object settings. AIMenuController, AIToolbarController, AISoundController +(AdiumSound, AdiumSoundSets, AdiumSpeech), AIDockController, +AIEmoticonController, ESContactAlertsController, ESFileTransferController, +ESDebugController and AILoginController are what their names say. + +## Where the bigger things are + +The contact list window: AIListController and AIListWindowController, with +AIStandardListWindowController and AIBorderlessListWindowController for the +two window styles; AISCLViewPlugin is the component that owns it. + +The preferences window: AIModernPreferencesWindowController, the fork's, +with AIPreferenceWindowController kept as a thin front to it. Panes are +AIPreferencePane subclasses spread across Source/ and Plugins/. Installed +xtras are managed there too: AIXtrasPreferences, AIXtraDetailsPage, +AIXtrasManager, and XtrasInstaller for the files. + +OTR: AdiumOTREncryption on libotr, with the ESOTR* windows and preferences +and OTRCommon.h. OMEMO is not here; it is in Plugins/Purple Service. + +Voice notes: AIVoiceNotePlugin, AIVoiceNoteShelfView, AIVoiceRecorder and +AIOpusEncoder. + +Logging: AILoggerPlugin writes the XML chat logs; AILogViewerWindowController, +AIChatLog, AILogFromGroup and AILogToGroup read them back; AIXMLAppender and +AIXMLChatlogConverter are the file format. + +Inline pictures in a conversation: AIInlineMediaLinkPlugin. Now-playing +status: ESiTunesPlugin. Global hotkeys: the SG* classes. First run: +AdiumSetupWizard. + +## The components + +Most *Plugin classes here are one feature each, and the fastest way to find +a feature is the list in AICoreComponentLoader.m, then the class it names. +A new feature is a new AIPlugin subclass added to that list. The other +components live in the folders under Plugins/.