Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 49 additions & 0 deletions Dependencies/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 64 additions & 0 deletions Frameworks/Adium/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
77 changes: 77 additions & 0 deletions Plugins/Purple Service/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
70 changes: 70 additions & 0 deletions Source/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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/.