Abuja Digital Studio · Est. 2018
Start a Project
Docs
OForum
Structured community forum for WordPress — rooms, threads, and replies with taxonomy-based categorization, moderation tools, user notifications, and profile integration. No BuddyPress required.
oforum
Developer documentation

OForum v1.0.0

A modern community discussion forum for WordPress with rooms, threads, replies, reactions, upvotes, bookmarks, @mentions, trust levels, shadow banning, rate limits, spam filter, support mode, daily / weekly digests, full-text search, and a REST API. Runs on plain WordPress with vanilla JS.

WordPress plugin WP 6.0 · PHP 8.0+ GPL-2.0+ Released 2026-04-25
01 · Overview

What OForum does

A full forum system built entirely on WordPress CPTs with first-party code from post types to REST routes.

🏠
Forum Rooms
Nested sub-rooms, visibility controls, role-gating, emoji icons
📌
Thread Types
Discussion, Question, Announcement, Showcase, plus sticky & closed
Accepted Solutions
Mark a reply as the accepted answer on Question threads
😊
Emoji Reactions
Configurable emoji set, toggle-to-remove, per-reply counts
🔔
Subscriptions
Follow rooms or threads, in-forum bell, @mention notifications
🛡️
Moderation
Report queue, shadow ban, IP ban, audit log
⬆️
Trust Levels
4-level auto-recalculated system with configurable thresholds
🔍
Full-Text Search
Filters by room, type, author, date, with keyword highlights in results
🌐
SEO
BreadcrumbList schema, DiscussionForumPosting, Yoast + SEO Framework sitemap
REST API
Full CRUD for rooms, threads, replies, reactions, and search
📐
Shortcodes
[oforum], [oforum_room], [oforum_thread]
🚫
No jQuery
Frontend runs on vanilla JS with no framework dependency
02 · Installation

Getting installed

Manual upload

1
Upload the oforum/ folder to /wp-content/plugins/.
2
Go to Plugins → Installed Plugins and activate OForum. On activation: registers CPTs (of_room, of_thread, of_reply), creates 10 database tables (reactions, subscriptions, notifications, reports, ip_bans, mod_log, votes, bookmarks, room_members, thread_stats), creates a /forum page with the index template, and flushes rewrite rules.
3
Navigate to OForum in the WordPress admin sidebar.
4
Create your first rooms under Rooms → Add Room.

Via ZIP upload

Go to Plugins → Add New → Upload Plugin, select oforum.zip, click Install Now, then Activate.

03 · Quick Start

Up and running in minutes

1
Create rooms. OForum → Rooms → Add Room. Give each room a name, optional icon/emoji, visibility setting, and colour accent.
2
Add rooms to your menu. Appearance → Menus → link to /forum/room-slug/.
3
Test posting. Log in as a subscriber, visit /forum/your-room/ and click + New Thread.
4
Configure settings. OForum → Settings to set replies-per-page, editor preferences, and trust thresholds.
ℹ️The forum index is at /forum/. If the page was not created on activation, go to OForum → Settings and the plugin will recreate it on the next page load.
04 · Architecture

Plugin architecture

oforum/ ├── oforum.php Main plugin entry point ├── admin/ │ ├── class-of-admin.php Admin panel controller (7 tabs) │ └── views/ │ ├── view-dashboard.php Dashboard tab │ ├── view-rooms.php Room management │ ├── view-threads.php Thread management │ ├── view-moderation.php Moderation queue, IP bans, log │ ├── view-analytics.php Analytics charts and tables │ ├── view-members.php Member roster + trust levels │ └── view-settings.php Plugin settings ├── includes/ │ ├── class-of-cpt.php CPT registration + DB table creation │ ├── class-of-taxonomy.php of_category + of_tag taxonomies │ ├── class-of-rooms.php Room CRUD and query helpers │ ├── class-of-room-members.php Private room rosters + per-user roles │ ├── class-of-query.php Thread and reply queries (uses thread_stats cache) │ ├── class-of-permissions.php Permission levels, IP ban checks │ ├── class-of-trust.php Trust level recalculation (4 tiers) │ ├── class-of-moderation.php Report queue, shadow ban, audit log │ ├── class-of-notify.php Subscriptions, notifications, @mention parsing │ ├── class-of-digest.php Cron-batched daily / weekly email digests │ ├── class-of-reactions.php Emoji reaction toggle and counts │ ├── class-of-votes.php Upvote / downvote on threads + replies │ ├── class-of-bookmarks.php Per-user saved threads │ ├── class-of-rate-limit.php Per-user rate limits (replies/min, threads/hour) │ ├── class-of-spam.php Link caps + Akismet bridge + report-threshold autoflag │ ├── class-of-support-mode.php Staff badges, SLA labels, KB conversion │ ├── class-of-integrations.php OEngage XP + OMailer segment hooks │ ├── class-of-search.php Full-text search with filters │ ├── class-of-seo.php Breadcrumbs, schema, sitemap │ ├── class-of-shortcode.php Shortcode handlers │ └── class-of-rest.php REST API routes ├── templates/ │ ├── forum-index.php Forum index page │ ├── room-page.php Individual room page │ ├── thread-page.php Individual thread page │ ├── profile-page.php User profile page │ └── partials/ │ ├── thread-card.php Thread card (room listing) │ ├── reply-block.php Single reply block │ ├── new-thread-form.php Thread submission form │ ├── new-reply-form.php Reply submission form │ └── report-modal.php Content report modal └── assets/ ├── css/ │ ├── of-admin-v2.css Admin panel styles │ └── of-front.css Frontend styles └── js/ ├── of-admin-v2.js Admin panel interactions └── of-front.js Frontend (vanilla JS, no jQuery)

Data model

CPTpost_typeVisibilityNotes
Forum Roomof_roomPrivateSettings stored in post_meta
Forum Threadof_threadPublicSEO-friendly URL: /forum/thread/[slug]
Forum Replyof_replyPrivateManaged entirely via plugin, not WP admin

Thread → room: post_meta key of_thread_room_id. Reply → thread: of_reply_thread_id. Reply hierarchy (quoting): of_reply_parent_id.

05 · Admin Panel

Admin panel

A standalone top-nav interface registered as a single add_menu_page() entry, with no sidebar sub-items. Matches the Orravo admin UI pattern.

RowContents
Brand barOForum icon + name + version badge + light/dark theme toggle
Top navDashboard · Rooms · Threads · Moderation · Analytics · Members · Settings

The Moderation tab shows a live badge with the count of pending reports + pending approval posts. Theme preference is saved to localStorage under of_admin_theme; dark mode applies class of-dark to #of-wrap.

06 · Forum Structure

Rooms & structure

Rooms are of_room CPT posts. Settings are stored as post meta. Sub-rooms (one level of nesting) appear indented under their parent on the forum index.

Room properties

PropertyMeta keyDescription
Namepost_titleDisplay name
Slugpost_nameURL slug
Descriptionpost_contentRoom description
Iconof_room_iconEmoji or text icon
Colorof_room_colorHex colour for accent
Visibilityof_room_visibilitypublic, members, or role
Required roleof_room_required_roleWP role slug for role-gated rooms
Sub-room ofpost_parentParent room ID (0 = top-level)
Sort ordermenu_orderDisplay sort order

PHP API

PHP// Create a room
$room_id = OF_Rooms::create([
    'name'       => 'General Discussion',
    'icon'       => '💬',
    'color'      => '#38BDF8',
    'visibility' => 'public',
]);

// Query rooms
$rooms = OF_Rooms::get_all();               // All rooms (flat)
$rooms = OF_Rooms::get_all(false);          // Top-level only
$room  = OF_Rooms::get($room_id);           // By ID
$room  = OF_Rooms::get_by_slug('general-discussion');

// Visibility check
$can   = OF_Rooms::user_can_view($room_id, $user_id);
07 · Thread System

Thread system

Threads are of_thread CPT posts at URL /forum/thread/[slug]/. Thread type controls the display badge and behavior.

Thread meta keys

KeyTypeDescription
of_thread_room_idintRoom the thread belongs to
of_thread_typestringdiscussion, question, announcement, showcase
of_thread_stickyint1 = pinned to top of room
of_thread_closedint1 = no new replies (except mods)
of_thread_solvedint1 = has an accepted solution
of_thread_solution_idintReply ID of the accepted solution
of_thread_reply_countintCached reply count
of_thread_viewsintView count
of_thread_last_replydatetimeLast reply timestamp (for sorting)
of_thread_last_reply_uidintUser ID of the last replier

Querying threads

PHP$result = OF_Query::get_threads([
    'room_id'      => 5,
    'thread_type'  => 'question',
    'sticky_first' => true,
    'page'         => 1,
    'per_page'     => 25,
    'orderby'      => 'last_reply',  // last_reply | date | reply_count
]);

// Returns:
// [
//   'threads'   => [...],  // array of formatted thread arrays
//   'total'     => 42,
//   'max_pages' => 2,
// ]

Admin thread actions (AJAX)

All admin thread actions use the of_thread_action AJAX endpoint with an action_type param:

action_typeEffect
stickyToggle sticky (pinned to top)
closeToggle closed (no new replies)
deleteTrash the thread
approvePublish a pending thread
08 · Reply System

Reply system

Replies are of_reply CPT posts (not public). All interactions go through the plugin's REST API and AJAX handlers, never through the standard WP admin.

Reply meta keys

KeyTypeDescription
of_reply_thread_idintParent thread ID
of_reply_parent_idintQuoted reply ID (0 = top-level)
of_reply_edited_atdatetimeLast edit timestamp
of_reply_is_solutionint1 = accepted as solution
of_shadow_bannedint1 = shadow banned (only author sees it)

Accepted solutions

Thread authors and moderators can mark a reply as the accepted answer on Question threads. This sets of_reply_is_solution = 1 on the reply, and of_thread_solved = 1 + of_thread_solution_id on the thread. The solution reply is pinned above pagination with a green ✓ Accepted Answer badge.

Edit window

Users can edit their own replies within a configurable window (default: 30 minutes). After the window, only moderators can edit. Edited replies show (edited) next to the timestamp. Set to 0 in Settings to disable editing entirely.

Reply quoting

Clicking Quote stores the reply ID in #of-quoted-reply-id and prefills the form with a quote preview. The of_reply_parent_id is set on submission to maintain the reply hierarchy.

09 · Reactions

Emoji reactions

Stored in wp_of_reactions. Toggling the same emoji again removes the reaction. One reaction per emoji per user per reply, enforced by a unique DB constraint.

PHP API

PHP// Toggle a reaction (returns action + updated counts)
$result = OF_Reactions::toggle($user_id, $reply_id, '👍');
// ['action' => 'added'|'removed', 'counts' => ['👍' => 3, '❤️' => 1]]

// Get counts for a reply
$counts = OF_Reactions::get_counts($reply_id);

// Get emojis a user has reacted with
$user_reactions = OF_Reactions::get_user_reactions($user_id, $reply_id);

REST API

HTTPPOST /wp-json/oforum/v1/reactions
{ "reply_id": 42, "emoji": "👍" }

// Response:
{ "action": "added", "counts": { "👍": 3, "❤️": 1 } }

Configure available emojis at Settings → Reactions → Available Emojis as a comma-separated list. Default: 👍,❤️,😂,🔥,💡. Override programmatically with the of_allowed_emojis filter.

10 · Subscriptions & Notifications

Subscriptions & notifications

Subscription types & frequencies

TypeNotified when
roomAny new thread posted in the room
threadAny new reply posted in the thread
FrequencyBehaviour
immediateEmail sent immediately on each new post
dailyQueued; cron oforum_digest_daily sends a batched 08:00 digest
weeklyQueued; cron oforum_digest_weekly sends a Monday 08:00 digest
noneIn-forum notification only, no email
PHPOF_Notify::subscribe($user_id, 'thread', $thread_id, 'immediate');
OF_Notify::unsubscribe($user_id, 'thread', $thread_id);
$is_subbed = OF_Notify::is_subscribed($user_id, 'thread', $thread_id);

In-forum notification types

TypeTrigger
new_replySomeone replied to a thread you follow
mentionSomeone @mentioned you
moderation_noticeA moderator actioned one of your posts
solution_acceptedYour reply was marked as the solution
PHPOF_Notify::create($user_id, [
    'type'      => 'mention',
    'post_id'   => $post_id,
    'from_user' => $author_id,
    'message'   => 'John Doe mentioned you.',
    'url'       => get_permalink($post_id) . '#reply-' . $reply_id,
]);

@mentions & polling

On reply submit, OF_Notify::parse_mentions() scans for @username patterns, resolves users by user_login and user_slug, and fires immediate in-forum notifications. The notification bell polls for unread counts every 60 seconds via AJAX (of_get_notifications).

11 · Moderation

Moderation tools

Permission levels

ConstantLevelLabelDescription
LEVEL_BANNED-1BannedCannot post; IP ban check runs here
LEVEL_NEW0New MemberFirst posts need moderator approval
LEVEL_MEMBER1MemberFull posting permissions
LEVEL_MOD2ModeratorCan approve, warn, delete, shadow-ban
LEVEL_ADMIN3AdminWordPress admins auto-receive this level
PHPOF_Permissions::set_level($user_id, OF_Permissions::LEVEL_MOD);

Report queue

Users click Report on any reply to file a report. Moderators see the queue at Moderation → Flagged Reports with four actions:

ActionEffect
✓ ApprovePublish the post
⚠ WarnSend a moderation email to the author (no post action)
🗑 DeleteTrash the post
✕ DismissDismiss the report without action

Shadow ban

Shadow-banned users' future posts are only visible to themselves and moderators. Other users see nothing, so the banned user has no idea they're banned.

PHPOF_Moderation::shadow_ban_user($user_id, $mod_user_id);
OF_Moderation::unshadow_ban_user($user_id, $mod_user_id);
$is_banned = OF_Moderation::is_shadow_banned($user_id);

IP ban

PHP// Permanent ban
OF_Moderation::ban_ip('192.168.1.1', $mod_id, 'Spam', null);

// Timed ban: expires 2026-12-31
OF_Moderation::ban_ip('192.168.1.1', $mod_id, 'Abuse', '2026-12-31 23:59:59');

OF_Moderation::unban_ip('192.168.1.1', $mod_id);
$is_banned = OF_Permissions::is_ip_banned('192.168.1.1');

IP bans block the post/reply REST API endpoints. The auto-moderation hook also checks IPs on reply submission.

Audit log

Every moderation action is logged to wp_of_mod_log. View at Moderation → Audit Log.

PHP$log = OF_Moderation::get_log(50); // Last 50 entries
12 · Trust Level System

Trust level system

Trust levels are automatically recalculated every time a user posts a thread or reply. Thresholds are configurable at Settings → Trust Level Thresholds.

LevelConstantLabelDefault threshold
0OF_Trust::LEVEL_NEWNew Member0 posts; first post needs approval
1OF_Trust::LEVEL_MEMBERMember5 posts
2OF_Trust::LEVEL_REGULARRegular25 posts
3OF_Trust::LEVEL_LEADERLeader100 posts

Trust gates

ActionMinimum trust required
Post threads and repliesLevel 1 Member
Post links in contentLevel 1 Member
Edit own reply (within window)All levels
First post requires approvalLevel 0 New Member only

PHP API

PHP$level         = OF_Trust::get_level($user_id);        // int 0–3
$label         = OF_Trust::get_label($level);           // "New Member", "Member", …
OF_Trust::recalculate($user_id);                        // Force recalculate
$needs_approval = OF_Trust::requires_approval($user_id); // bool
14 · SEO

SEO integration

Breadcrumbs

PHPOF_SEO::render_breadcrumbs();             // Forum index
OF_SEO::render_breadcrumbs($room);        // Room page
OF_SEO::render_breadcrumbs($room, $thread); // Thread page

Renders BreadcrumbList Schema.org markup on all forum pages.

DiscussionForumPosting schema

On thread pages, OF_SEO::inject_schema() outputs a <script type="application/ld+json"> block with DiscussionForumPosting structured data: headline, author, date, reply count, and first 5 reply comments.

Page titles & sitemap

The plugin filters document_title_parts and wp_title for proper titles: Thread Title · Site Name and Room Name · Forum · Site Name.

OForum integrates with Yoast SEO (wpseo_sitemap_index_links) and The SEO Framework (the_seo_framework_sitemap_urls) to include the 200 most recently modified published thread URLs in the sitemap.

15 · REST API

REST API reference

Base URL: /wp-json/oforum/v1/. All write endpoints require WordPress authentication via X-WP-Nonce header or cookie-based auth.

Rooms

MethodEndpointAuthDescription
GET/roomsNoneList all rooms
GET/rooms/{id}NoneGet single room
POST/roomsAdminCreate room
PUT/rooms/{id}AdminUpdate room
DELETE/rooms/{id}AdminDelete room

Threads

MethodEndpointAuthDescription
GET/threads?room_id=XNoneList threads (paginated)
GET/threads/{id}NoneGet single thread
POST/threadsLogged inCreate thread
PUT/threads/{id}Author or ModUpdate thread
DELETE/threads/{id}ModeratorDelete thread
JSON// Create thread body
{
  "title":   "My thread title",
  "content": "<p>Thread content</p>",
  "type":    "discussion",
  "room_id": 5
}

Replies

MethodEndpointAuthDescription
GET/replies/{id}NoneGet single reply
POST/repliesLogged inPost reply
PUT/replies/{id}Author (within window)Edit reply
DELETE/replies/{id}Author or ModDelete reply
JSON// Create reply body
{
  "thread_id": 42,
  "parent_id": 0,
  "content":   "<p>My reply</p>"
}

Reactions & search

MethodEndpointAuthDescription
POST/reactionsLogged inToggle emoji reaction. Body: {"reply_id":42,"emoji":"👍"}
GET/search?q=termNoneFull-text search. Params: q, room_id, page, per_page
16 · Shortcodes

Shortcodes

[oforum]
Renders the full forum index (all rooms list).
[oforum]
[oforum_room]
Renders a specific room with its thread list. Use either slug or id.
[oforum_room slug="general-discussion"]
[oforum_room id="5"]
[oforum_thread]
Renders a specific thread with its replies.
[oforum_thread id="42"]
17 · Frontend Templates

Frontend templates

Templates live in the templates/ directory. They load the active theme's header.php and footer.php via get_header() / get_footer().

URL routing

URL patternTemplate
/forum/templates/forum-index.php via page template
/forum/{room-slug}/templates/room-page.php via of_room rewrite var
/forum/thread/{thread-slug}/templates/thread-page.php via is_singular('of_thread')
/forum/profile/{username}/templates/profile-page.php via of_profile rewrite var

Frontend JS globals

The OF global object is available on all forum pages:

JSwindow.OF = {
  ajaxUrl:   'https://site.com/wp-admin/admin-ajax.php',
  restUrl:   'https://site.com/wp-json/oforum/v1/',
  nonce:     '...',       // WP REST nonce
  ajaxNonce: '...',       // AJAX nonce
  loggedIn:  1,           // 1 or 0
  userId:    42,
  loginUrl:  '...',
  forumUrl:  '/forum/',
  perPage:   25,
};

window.OF_emojis = ['👍', '❤️', '😂', '🔥', '💡']; // from settings
ℹ️Template overrides: Copy templates into your theme at your-theme/oforum/ to override them. (Template override detection is planned for v1.1.0; child-theme CSS overrides are the recommended approach in v1.0.0.)
18 · Settings Reference

Settings reference

All settings stored as WordPress options via get_option / update_option.

Option keyTypeDefaultDescription
of_replies_per_pageint25Replies loaded per page on thread view
of_edit_window_minutesint30Minutes users can edit their own replies (0 = disabled)
of_guest_readbool1Whether non-logged-in users can read public rooms
of_rich_text_enabledbool1Enable rich text toolbar in reply form
of_markdown_enabledbool0Enable Markdown mode toggle per user
of_reaction_emojisstring👍,❤️,😂,🔥,💡Comma-separated allowed emojis
of_akismet_enabledbool0Enable Akismet spam filtering (requires Akismet plugin)
of_trust_threshold_1int5Posts needed to reach Level 1 (Member)
of_trust_threshold_2int25Posts needed to reach Level 2 (Regular)
of_trust_threshold_3int100Posts needed to reach Level 3 (Leader)
of_rate_reply_per_minint3Max replies per user per minute
of_rate_reply_per_hourint20Max replies per user per hour
of_rate_thread_per_hourint5Max threads per user per hour
of_spam_max_links_newint1Max links per post for New Member trust level
of_spam_max_links_trustedint5Max links per post for Member+ trust levels
of_spam_report_thresholdint3Reports needed to auto-flag a post for review
of_xp_thread_createint10OEngage XP awarded per new thread
of_xp_reply_postint5OEngage XP awarded per reply
of_xp_solution_acceptedint25OEngage XP awarded when a reply is marked as the solution
of_staff_members_globalstring(empty)Comma-separated user IDs treated as forum-wide support staff
of_staff_members_{room_id}string(empty)Per-room staff override (support mode)
of_room_sla_hours (post meta)int0Hours before a thread without a staff reply is flagged in support mode
19 · Filters & Action Hooks

Filters & actions

Filters

PHP// Modify allowed reaction emojis
add_filter('of_allowed_emojis', function($emojis) {
    return ['👍', '❤️', '🎉', '🤔', '👀'];
});

// Modify room visibility check
add_filter('of_room_can_view', function($can, $room_id, $user_id) {
    // Custom logic; return bool
    return $can;
}, 10, 3);

// Modify reply content before save
add_filter('of_reply_content', function($content, $user_id) {
    // e.g. strip disallowed HTML
    return $content;
}, 10, 2);

Actions

PHP// Fired after a thread is created
add_action('of_thread_created', function($thread_id, $room_id, $author_id) {
    // e.g. post to Slack channel
}, 10, 3);

// Fired after a reply is created and published
add_action('of_reply_created', function($reply_id, $thread_id, $author_id) {
    // e.g. award points in a gamification plugin
}, 10, 3);

// Fired after trust level changes (4 args: user_id, new_level, post_count, solved_count)
add_action('oforum_trust_level_updated', function($user_id, $level, $posts, $solved) {
    // e.g. award badge in OEngage when a user reaches Leader
}, 10, 4);

// Fired when a moderator marks a reply as the accepted solution
add_action('oforum_solution_marked', function($thread_id, $reply_id, $solver_uid) {
    // e.g. notify the reply author + bump their reputation
}, 10, 3);

// Fired by OF_Notify when a notification gets pushed (in-forum or email)
add_action('oforum_push_event', function($user_id, $data) {
    // e.g. forward to your real-time push provider
}, 10, 2);
⚠️Hooks not yet fired in v1.0.0: oforum_thread_created, oforum_reply_posted, oforum_report_filed, and of_trust_level_changed are registered for OEngage / OMailer integrations but the calling sites still need wiring. For now, use the CPT hooks save_post_of_thread and save_post_of_reply directly. Tracked for v1.1.0.
20 · Database Schema

Database schema

10 custom tables in addition to the standard WP tables used for the CPTs.

of_reactions

ColumnTypeDescription
idBIGINT UNSIGNED AI PK
user_idBIGINT UNSIGNEDReacting user
reply_idBIGINT UNSIGNEDReply being reacted to
emojiVARCHAR(10)Emoji character
created_atDATETIMEWhen the reaction was added

Unique constraint on (user_id, reply_id, emoji): one reaction per emoji per user per reply.

of_subscriptions

ColumnTypeDescription
user_idBIGINT UNSIGNEDSubscriber
sub_typeVARCHAR(20)thread or room
object_idBIGINT UNSIGNEDThread ID or room post ID
frequencyVARCHAR(20)immediate, daily, weekly, none
created_atDATETIME

of_notifications

ColumnTypeDescription
user_idBIGINT UNSIGNEDRecipient
typeVARCHAR(50)new_reply, mention, moderation_notice, solution_accepted
post_idBIGINT UNSIGNEDRelated post ID
from_userBIGINT UNSIGNEDSender user ID
messageTEXTNotification text
urlVARCHAR(500)Click-through URL
is_readTINYINT(1)0 = unread, 1 = read
email_sentTINYINT(1)0 = pending, 1 = sent
created_atDATETIME

of_reports

ColumnTypeDescription
reporter_idBIGINT UNSIGNEDUser who filed the report
post_idBIGINT UNSIGNEDReported post
post_typeVARCHAR(20)of_thread or of_reply
reasonVARCHAR(50)spam, offensive, misinformation, off-topic, other
detailTEXTOptional additional context
statusVARCHAR(20)pending, dismissed, actioned
reviewed_byBIGINT UNSIGNEDModerator who reviewed
reviewed_atDATETIMEWhen reviewed
created_atDATETIMEWhen filed

of_ip_bans & of_mod_log

TableKey columns
of_ip_bansip_address (UNIQUE), reason, banned_by, expires_at (NULL = permanent)
of_mod_logmod_id, action (e.g. shadow_ban, ip_ban, dismiss_report), target_id, target_type, detail

of_votes

ColumnTypeDescription
user_idBIGINT UNSIGNEDVoting user
post_idBIGINT UNSIGNEDThread or reply ID
post_typeVARCHAR(20)of_thread or of_reply
voteTINYINT1 = upvote, -1 = downvote, 0 = removed
created_atDATETIME

Unique constraint on (user_id, post_id, post_type): one vote per user per post.

of_bookmarks

ColumnTypeDescription
user_idBIGINT UNSIGNEDBookmarking user
thread_idBIGINT UNSIGNEDSaved thread
created_atDATETIME

Unique constraint on (user_id, thread_id): a thread can be bookmarked at most once per user.

of_room_members

ColumnTypeDescription
room_idBIGINT UNSIGNEDPrivate room ID
user_idBIGINT UNSIGNEDMember user ID
roleVARCHAR(20)member, mod, owner
created_atDATETIME

Backs invite-only rooms. Public rooms ignore this table; visibility is gated by of_room_visibility meta.

of_thread_stats

ColumnTypeDescription
thread_idBIGINT UNSIGNED PKThread post ID
room_idBIGINT UNSIGNEDOwning room ID (denormalised for room listings)
reply_countINT UNSIGNEDCached reply count
view_countINT UNSIGNEDPage-view counter, incremented on thread page load
vote_scoreINTCached upvote minus downvote score
last_reply_atDATETIMELatest reply timestamp
last_reply_uidBIGINT UNSIGNEDLatest reply author ID
updated_atDATETIMEAuto-updated by OF_Query::increment_view() + reply / vote handlers

Performance cache so room listings and thread cards do not re-aggregate post meta on every render.

21 · Uninstall

Uninstall & cleanup

When the plugin is deleted (not just deactivated), of_uninstall() permanently removes all forum data.

1
Drops all 10 custom tables.
2
Deletes all of_room, of_thread, of_reply posts and their post meta.
3
Deletes all options with the of_ prefix.
⚠️Warning: This permanently removes all forum data. There is no undo. Export your data before uninstalling. To deactivate without data loss, use Plugins → Deactivate (not Delete).
22 · Changelog

What's shipped

v1.0.0 Initial release · 2026-04-25
  • Full forum room + thread + reply system with of_room, of_thread, of_reply CPTs
  • of_category (hierarchical) + of_tag (flat) taxonomies attached to threads
  • Thread types: discussion, question, announcement, showcase
  • 4-level trust system (New / Member / Regular / Leader) with auto-recalculation on every post
  • Emoji reactions: configurable set, toggle-to-remove, per-reply counts
  • Upvote / downvote on threads + replies with cached vote scores
  • Per-user bookmarks for saved threads
  • Per-user rate limits (replies/min, replies/hour, threads/hour)
  • Spam filter: link caps by trust level, report-threshold autoflag, optional Akismet bridge
  • Full-text search with filters (room, type, author, date range) + keyword highlights
  • Thread and room subscriptions with immediate, daily, or weekly cadence
  • Cron-batched daily / weekly email digests (oforum_digest_daily, oforum_digest_weekly)
  • In-forum notification bell with unread count badge + dropdown
  • @mention detection with immediate in-forum notification
  • Reply quoting: prefills form with quoted text + sets parent ID
  • Thread reading position memory (localStorage)
  • Accepted solution marking on question threads, pinned above pagination
  • Moderation queue: report, dismiss, warn, delete; bulk actions across threads + replies
  • Shadow ban + IP ban (with optional expiry) + moderation audit log
  • Support mode: per-room staff lists, staff badges, SLA labels, KB conversion AJAX
  • Private rooms with per-user member roster + roles (of_room_members)
  • Thread-stats cache (of_thread_stats) for fast room listings without post-meta fan-out
  • Standalone admin panel with 7-tab top nav: Dashboard, Rooms, Threads, Moderation, Analytics, Members, Settings
  • Dark / light theme toggle in admin (localStorage persistence)
  • Cmd+K command palette registration via Orravo Core (OC_Shell)
  • REST API: full CRUD for rooms, threads, replies, votes, bookmarks, reactions, search
  • OEngage XP integration: configurable XP per thread, reply, accepted solution, trust promotion
  • OMailer integration: contact-profile sections + segment conditions for trust level / activity
  • SEO: BreadcrumbList schema, DiscussionForumPosting schema, Yoast + SEO Framework sitemap integration
  • Analytics dashboard: posts/day, peak hours, top users, room stats
  • Shortcodes: [oforum], [oforum_room], [oforum_thread]
  • Clean uninstall hook: drops 10 tables, CPT posts, and all of_* options
  • No jQuery dependency on frontend; vanilla JS throughout
  • PHP 8.0+ required, WordPress 6.0+ required
  • Multisite compatible
✦ Need help?

Got a question about OForum?

Reach out directly. Kenneth replies within 24 hours.