Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md

Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
2026-07-07 14:44:12 +08:00
parent d4ef996f49
commit aeddedf2a3
1348 changed files with 176 additions and 57 deletions
+29
View File
@@ -0,0 +1,29 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
"gorm.io/datatypes"
)
// ChannelAPI represents a REST API channel configuration.
// Reference: Chatwoot Channel::Api + P2B M2 spec
type ChannelAPI struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
WebhookURL string `gorm:"size:512" json:"webhook_url"`
Secret string `gorm:"size:255" json:"secret"`
HMACToken string `gorm:"size:255" json:"hmac_token"`
HMACMandatory bool `gorm:"column:hmac_mandatory;default:false" json:"hmac_mandatory"`
AdditionalAttributes datatypes.JSON `gorm:"type:jsonb;default:'{}'" json:"additional_attributes"`
Identifier string `gorm:"size:255" json:"identifier"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelAPI) TableName() string { return "channel_api" }
func (c ChannelAPI) GetInboxID() uint { return c.InboxID }
func (c ChannelAPI) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeAPI }
+25
View File
@@ -0,0 +1,25 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelAPI represents a REST API channel configuration.
// Reference: Chatwoot Channel::Api + P2B M2 spec
type ChannelAPI struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
WebhookURL string `gorm:"size:512" json:"webhook_url"`
HMACToken string `gorm:"size:255" json:"hmac_token"`
Identifier string `gorm:"size:255" json:"identifier"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelAPI) TableName() string { return "channel_api" }
func (c ChannelAPI) GetInboxID() uint { return c.InboxID }
func (c ChannelAPI) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeApi }
@@ -0,0 +1,32 @@
package channel
import (
"time"
"gorm.io/gorm"
)
// ChannelBase contains common fields shared across all channel types.
// This mirrors Chatwoot's "channelable" concern pattern where each channel
// type (web_widget, facebook, twitter, etc.) embeds a base and adds its
// own specific fields.
type ChannelBase struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
AccountID uint `gorm:"index;not null" json:"account_id"`
InboxID uint `gorm:"index;not null" json:"inbox_id"`
ChannelType string `gorm:"size:50;index;not null" json:"channel_type"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}
func (ChannelBase) TableName() string { return "channels" }
// Channelable is the interface that all channel type models must implement.
// Each channel (WebWidget, FacebookPage, TwitterProfile, etc.) embeds
// ChannelBase and implements this interface to return its specific type.
type Channelable interface {
GetChannelBase() ChannelBase
GetChannelType() string
TableName() string
}
+162
View File
@@ -0,0 +1,162 @@
package channel
import (
"fmt"
"github.com/gochat/gochat/internal/model"
)
// ChannelEmail represents an Email (SMTP/IMAP) channel configuration.
// Reference: Chatwoot app/models/channel/email.rb
//
// Chatwoot's ChannelEmail model attributes:
// - email: The inbound email address (e.g. support@acme.com)
// - forward_to_address: Email address for forwarding (alternative to IMAP)
// - imap_enabled: Whether IMAP polling is enabled
// - imap_address: IMAP server hostname
// - imap_port: IMAP server port (typically 993 for SSL)
// - imap_login: IMAP login/username
// - imap_password: IMAP password
// - imap_ssl: Whether to use SSL for IMAP connection
// - smtp_enabled: Whether SMTP sending is enabled
// - smtp_address: SMTP server hostname
// - smtp_port: SMTP server port (typically 587 for TLS)
// - smtp_login: SMTP login/username
// - smtp_password: SMTP password
// - smtp_ssl: Whether to use SSL/TLS for SMTP
//
// Chatwoot lifecycle hooks:
// - before_validation :ensure_forward_to_address (sets default forwarding address)
// - before_create :ensure_imap_connection (validates IMAP credentials)
// - before_create :ensure_smtp_connection (validates SMTP credentials)
//
// gochat enhancements beyond Chatwoot:
// - mailbox_name: Friendly name for the mailbox (for display)
// - imap_ssl_mode: Explicit SSL mode (none/starttls/ssl) instead of boolean
// - smtp_ssl_mode: Explicit SSL mode instead of boolean
// - domain: Email domain for routing (e.g. "acme.com")
// - use_reply_to: Whether to use Reply-To header for contact identification
// ChannelEmail stores Email channel configuration for SMTP/IMAP.
type ChannelEmail struct {
model.Base
// Account foreign key — links to the Account that owns this channel
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
// Inbound email address — the address customers send emails to
// Reference: Chatwoot EDITABLE_ATTRS = [:email]
Email string `gorm:"size:255;not null" json:"email"`
// Friendly name for the mailbox (e.g. "Acme Support")
MailboxName string `gorm:"size:255" json:"mailbox_name,omitempty"`
// Email domain for routing (extracted from Email, e.g. "acme.com")
Domain string `gorm:"size:255;index" json:"domain"`
// === IMAP Configuration ===
// Whether IMAP polling is enabled
IMAPEnabled bool `gorm:"default:false" json:"imap_enabled"`
// IMAP server hostname (e.g. "imap.gmail.com")
IMAPAddress string `gorm:"size:255" json:"imap_address,omitempty"`
// IMAP server port (typically 993 for SSL, 143 for plain)
IMAPPort int `gorm:"default:993" json:"imap_port,omitempty"`
// IMAP login/username
IMAPLogin string `gorm:"size:255" json:"imap_login,omitempty"`
// IMAP password (stored encrypted at application level)
IMAPPassword string `gorm:"size:255" json:"imap_password,omitempty"`
// IMAP SSL mode: "none", "starttls", "ssl"
IMAPSSLMode string `gorm:"size:20;default:ssl" json:"imap_ssl_mode,omitempty"`
// IMAP mailbox folder to monitor (e.g. "INBOX")
IMAPFolder string `gorm:"size:255;default:INBOX" json:"imap_folder,omitempty"`
// === SMTP Configuration ===
// Whether SMTP sending is enabled
SMTPEnabled bool `gorm:"default:false" json:"smtp_enabled"`
// SMTP server hostname (e.g. "smtp.gmail.com")
SMTPAddress string `gorm:"size:255" json:"smtp_address,omitempty"`
// SMTP server port (typically 587 for STARTTLS, 465 for SSL, 25 for plain)
SMTPPort int `gorm:"default:587" json:"smtp_port,omitempty"`
// SMTP login/username
SMTPLogin string `gorm:"size:255" json:"smtp_login,omitempty"`
// SMTP password (stored encrypted at application level)
SMTPPassword string `gorm:"size:255" json:"smtp_password,omitempty"`
// SMTP SSL mode: "none", "starttls", "ssl"
SMTPSSLMode string `gorm:"size:20;default:starttls" json:"smtp_ssl_mode,omitempty"`
// === Behavioral Configuration ===
// Whether to use Reply-To header for contact identification
// When true, the Reply-To address is used instead of From address
UseReplyTo bool `gorm:"default:false" json:"use_reply_to,omitempty"`
// Whether to automatically group emails into conversations by thread
AutoThread bool `gorm:"default:true" json:"auto_thread,omitempty"`
// Maximum attachment size in bytes (0 = no limit)
MaxAttachmentSize int64 `gorm:"default:0" json:"max_attachment_size,omitempty"`
}
func (ChannelEmail) TableName() string { return "channel_emails" }
// GetChannelBase returns the ChannelBase for this channel.
func (c *ChannelEmail) GetChannelBase() ChannelBase {
return ChannelBase{
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: "email",
}
}
// GetChannelType returns the channel type identifier.
func (c *ChannelEmail) GetChannelType() string { return "email" }
// IsIMAPConfigured returns true if IMAP connection parameters are set.
func (c *ChannelEmail) IsIMAPConfigured() bool {
return c.IMAPEnabled && c.IMAPAddress != "" && c.IMAPLogin != "" && c.IMAPPassword != ""
}
// IsSMTPConfigured returns true if SMTP connection parameters are set.
func (c *ChannelEmail) IsSMTPConfigured() bool {
return c.SMTPEnabled && c.SMTPAddress != "" && c.SMTPLogin != "" && c.SMTPPassword != ""
}
// IMAPConnectionString returns a human-readable IMAP connection summary.
func (c *ChannelEmail) IMAPConnectionString() string {
if !c.IsIMAPConfigured() {
return "IMAP not configured"
}
return c.IMAPLogin + "@" + c.IMAPAddress + ":" + formatPort(c.IMAPPort)
}
// SMTPConnectionString returns a human-readable SMTP connection summary.
func (c *ChannelEmail) SMTPConnectionString() string {
if !c.IsSMTPConfigured() {
return "SMTP not configured"
}
return c.SMTPLogin + "@" + c.SMTPAddress + ":" + formatPort(c.SMTPPort)
}
// formatPort converts port int to string, using defaults if zero.
func formatPort(port int) string {
if port == 0 {
return "default"
}
return fmt.Sprintf("%d", port)
}
@@ -0,0 +1,35 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelEmail represents an email channel configuration.
// Reference: Chatwoot Channel::Email + P2B M2 spec
type ChannelEmail struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
IMAPEnabled bool `gorm:"default:false" json:"imap_enabled"`
IMAPEmail string `gorm:"size:255" json:"imap_email"`
IMAPPassword string `gorm:"size:255" json:"-"`
IMAPAddress string `gorm:"size:255" json:"imap_address"`
IMAPPort int `json:"imap_port"`
IMAPSSL bool `gorm:"default:false" json:"imap_ssl"`
SMTPEnabled bool `gorm:"default:false" json:"smtp_enabled"`
SMTPEmail string `gorm:"size:255" json:"smtp_email"`
SMTPPassword string `gorm:"size:255" json:"-"`
SMTPAddress string `gorm:"size:255" json:"smtp_address"`
SMTPPort int `json:"smtp_port"`
SMTPSSL bool `gorm:"default:false" json:"smtp_ssl"`
MailForwarding bool `gorm:"default:false" json:"mail_forwarding"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelEmail) TableName() string { return "channel_emails" }
func (c ChannelEmail) GetInboxID() uint { return c.InboxID }
func (c ChannelEmail) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeEmail }
@@ -0,0 +1,69 @@
package channel
import "github.com/gochat/gochat/internal/model"
// ChannelFacebook represents a Facebook Messenger channel.
// Reference: Chatwoot app/models/channel/facebook_page.rb
//
// Chatwoot's ChannelFacebook (Channel::FacebookPage) model attributes:
// - account_id: FK to Account
// - page_id: Facebook Page ID
// - page_access_token: Long-lived Page access token
// - page_name: Display name of the FB Page
// - reauthorization_required: Flag when token expires
//
// gochat enhancements beyond Chatwoot:
// - instagram_business_account_id: Optional link to IG account for cross-channel
// - app_id: FB App ID for webhook verification
// - webhook_verify_token: Token for FB webhook subscription verification
type ChannelFacebook struct {
model.Base
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
// Facebook Page ID — identifies the FB Page for Messenger
PageID string `gorm:"size:255;not null" json:"page_id"`
// Long-lived Page access token for Graph API calls
// Requires pages_messaging permission for Messenger
PageAccessToken string `gorm:"size:512;not null" json:"page_access_token"`
// Long-lived user access token used to discover and reauthorize pages.
UserAccessToken string `gorm:"size:512" json:"user_access_token,omitempty"`
// Display name of the Facebook Page
PageName string `gorm:"size:255" json:"page_name,omitempty"`
// FB App ID — used for webhook verification and API calls
AppID string `gorm:"size:255" json:"app_id,omitempty"`
// Webhook verify token — used during FB webhook subscription
// Reference: Chatwoot doesn't store this; GoChat adds for verification
WebhookVerifyToken string `gorm:"size:255" json:"webhook_verify_token,omitempty"`
// Instagram Business Account ID — links to IG account for cross-channel
// When set, this FB Page also manages Instagram DMs
// Reference: FB Graph API /me?fields=instagram_business_account
InstagramBusinessAccountID string `gorm:"size:255" json:"instagram_business_account_id,omitempty"`
// Tracks when the Page token needs reauthorization
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required"`
}
func (ChannelFacebook) TableName() string { return "channel_facebook_pages" }
// GetChannelBase returns the base channel fields for polymorphic access.
func (c *ChannelFacebook) GetChannelBase() ChannelBase {
return ChannelBase{
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: "facebook",
}
}
// GetChannelType returns the channel type identifier.
func (c *ChannelFacebook) GetChannelType() string { return "facebook" }
@@ -0,0 +1,27 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelFacebookPage represents a Facebook Messenger channel configuration.
// Reference: Chatwoot Channel::FacebookPage + P2B M2 spec
type ChannelFacebookPage struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
PageID string `gorm:"size:255;not null;index" json:"page_id"`
UserAccessToken string `gorm:"size:1024;not null" json:"-"`
AppID string `gorm:"size:255" json:"app_id"`
PageAccessToken string `gorm:"size:1024;not null" json:"-"`
ReauthRequired bool `gorm:"default:false" json:"reauth_required"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelFacebookPage) TableName() string { return "channel_facebook_pages" }
func (c ChannelFacebookPage) GetInboxID() uint { return c.InboxID }
func (c ChannelFacebookPage) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeFacebookPage }
+58
View File
@@ -0,0 +1,58 @@
package channel
import "github.com/gochat/gochat/internal/model"
// ChannelGoogle represents a Google (Google Chat/Gmail) channel.
// Reference: Chatwoot does not have a native Google Chat channel — this is a gochat addition.
//
// Google integration uses Google OAuth 2.0 for authentication
// and Google Chat API / Gmail API for messaging.
//
// Features:
// - Google OAuth 2.0 authorization flow
// - Google Chat space messaging via Chat API
// - Gmail integration potential (like email channel)
// - Google webhook subscriptions for real-time events
type ChannelGoogle struct {
model.Base
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
// Google user email or unique identifier
GoogleUserID string `gorm:"size:255;not null;uniqueIndex" json:"google_user_id"`
// Google OAuth 2.0 access token
AccessToken string `gorm:"size:2048;not null" json:"-"`
// OAuth 2.0 refresh token for token renewal
RefreshToken string `gorm:"size:2048" json:"-"`
// Google Chat space ID (optional — for Google Chat integration)
SpaceID string `gorm:"size:255" json:"space_id,omitempty"`
// Display name of the connected Google resource
DisplayName string `gorm:"size:255" json:"display_name,omitempty"`
// Webhook subscription ID for Google Chat events
SubscriptionID string `gorm:"size:255" json:"subscription_id,omitempty"`
// Flag indicating reauthorization is required
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required,omitempty"`
}
func (ChannelGoogle) TableName() string { return "channel_google" }
func (c ChannelGoogle) GetChannelBase() ChannelBase {
return ChannelBase{
ID: c.ID,
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: "google",
}
}
func (c ChannelGoogle) GetChannelType() string { return "google" }
@@ -0,0 +1,66 @@
package channel
import "github.com/gochat/gochat/internal/model"
// ChannelInstagram represents an Instagram DM channel.
// Reference: Chatwoot app/models/channel/instagram.rb
//
// Instagram DM uses the Facebook Graph API under the hood
// (Meta Business Suite / Instagram Graph API).
// The Instagram channel requires a connected Facebook Page
// since Instagram DMs are managed via the Page's Graph API permissions.
//
// Chatwoot's ChannelInstagram model attributes:
// - account_id: FK to Account
// - instagram_account_id: The IG-scoped user ID (IGID)
// - page_access_token: Token inherited from the connected FB Page
// - connected_fb_page_id: The Facebook Page that manages this IG account
//
// gochat enhancements beyond Chatwoot:
// - instagram_business_account_id: IG Business Account ID (distinct from IGID)
// - reauthorization_required: Tracks when token needs refresh
type ChannelInstagram struct {
model.Base
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
// Instagram-scoped user ID (IGID) — identifies the IG business account
// Reference: Chatwoot instagram_account_id attribute
InstagramAccountID string `gorm:"size:255;not null" json:"instagram_account_id"`
// Instagram Business Account ID — used for Graph API calls
// This is the ig_id returned by /me?fields=ig_id
InstagramBusinessAccountID string `gorm:"size:255" json:"instagram_business_account_id,omitempty"`
// Page access token inherited from the connected Facebook Page
// Instagram DM requires a Page access token with instagram_manage_messages permission
PageAccessToken string `gorm:"size:512;not null" json:"page_access_token"`
// Connected Facebook Page ID — the FB Page that manages this IG account
// Reference: Chatwoot connected_fb_page_id
ConnectedFBPageID string `gorm:"size:255;not null" json:"connected_fb_page_id"`
// Instagram account name (handle)
InstagramAccountName string `gorm:"size:255" json:"instagram_account_name,omitempty"`
// Tracks when the Page token needs reauthorization
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required"`
}
func (ChannelInstagram) TableName() string { return "channel_instagrams" }
// GetChannelBase returns the base channel fields for polymorphic access.
func (c *ChannelInstagram) GetChannelBase() ChannelBase {
return ChannelBase{
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: "instagram",
}
}
// GetChannelType returns the channel type identifier.
func (c *ChannelInstagram) GetChannelType() string { return "instagram" }
@@ -0,0 +1,26 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelInstagram represents an Instagram DM channel configuration.
// Reference: Chatwoot Channel::Instagram + P2B M2 spec
type ChannelInstagram struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
IGAccountID string `gorm:"size:255;not null;index" json:"ig_account_id"`
AccessToken string `gorm:"size:1024;not null" json:"-"`
IGHandle string `gorm:"size:255" json:"ig_handle"`
ReauthRequired bool `gorm:"default:false" json:"reauth_required"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelInstagram) TableName() string { return "channel_instagrams" }
func (c ChannelInstagram) GetInboxID() uint { return c.InboxID }
func (c ChannelInstagram) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeInstagram }
+57
View File
@@ -0,0 +1,57 @@
package channel
// ChannelLINE represents a LINE Official Account channel configuration.
// Reference: LINE Messaging API — https://developers.line.biz/en/docs/messaging-api/
//
// GoChat addition — LINE Messaging API integration.
// The LINE channel connects LINE Official Accounts to gochat conversations.
//
// Key LINE API concepts:
// - Channel Access Token: long-lived token for API calls (renewable)
// - Channel Secret: used for webhook signature verification (HMAC-SHA256)
// - LINE Login: OAuth2 flow for user authentication (optional)
// - Webhook URL: where LINE sends event notifications
//
// Design notes:
// - Uses GORM soft-delete pattern consistent with other channel models
// - JSON field tags follow snake_case convention per P2D §7
// - Channel-specific fields (channel_access_token, channel_secret) are stored
// in Inbox.ChannelConfig rather than in this model, keeping the model lean
// per the Chatwoot pattern where channel_type specific config lives in the polymorphic association
import "gorm.io/gorm"
// ChannelLINE is the GORM model for LINE Official Account channels.
// Reference: Chatwoot does not have LINE channel support — this is a GoChat addition.
// Design: Follows the same pattern as ChannelTelegram — minimal GORM model with
// config stored in Inbox.ChannelConfig (JSONB).
type ChannelLINE struct {
gorm.Model
// LINE Channel ID (from LINE Developers Console)
// Unique identifier for the LINE Official Account
ChannelID string `json:"channel_id" gorm:"type:varchar(255);uniqueIndex;not null"`
// LINE Official Account name (display name)
Name string `json:"name" gorm:"type:varchar(255)"`
// AccountID references the owning account (multi-tenant)
// Foreign key to accounts table for tenant isolation
AccountID uint `json:"account_id" gorm:"index;not null"`
// InboxID references the associated inbox (1:1 with channel)
InboxID uint `json:"inbox_id" gorm:"uniqueIndex;not null"`
}
// TableName overrides GORM table name convention.
func (ChannelLINE) TableName() string { return "channel_lines" }
// Channelable interface implementation for ChannelLINE.
// Returns LINE-specific identifiers and display name.
func (c *ChannelLINE) GetChannelType() string { return "line" }
func (c *ChannelLINE) GetChannelID() string { return c.ChannelID }
func (c *ChannelLINE) GetName() string { return c.Name }
func (c *ChannelLINE) GetAccountID() uint { return c.AccountID }
func (c *ChannelLINE) GetInboxID() uint { return c.InboxID }
func (c *ChannelLINE) SetInboxID(id uint) { c.InboxID = id }
@@ -0,0 +1,25 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelLine represents a LINE channel configuration.
// Reference: Chatwoot Channel::Line + P2B M2 spec
type ChannelLine struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
LineChannelID string `gorm:"size:255;not null;index" json:"line_channel_id"`
AccessToken string `gorm:"size:1024;not null" json:"-"`
ChannelSecret string `gorm:"size:255;not null" json:"-"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelLine) TableName() string { return "channel_lines" }
func (c ChannelLine) GetInboxID() uint { return c.InboxID }
func (c ChannelLine) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeLine }
@@ -0,0 +1,64 @@
package channel
import "github.com/gochat/gochat/internal/model"
// ChannelMicrosoft represents a Microsoft (Azure AD/Teams) channel.
// Reference: Chatwoot does not have a native Microsoft channel — this is a gochat addition.
//
// Enterprise Microsoft integration uses Azure AD OAuth for authentication
// and Microsoft Graph API for Teams messaging.
//
// Features:
// - Azure AD OAuth 2.0 authorization flow for enterprise SSO
// - Microsoft Teams channel messaging via Graph API
// - Outlook email integration potential
// - Azure webhook subscriptions for real-time events
type ChannelMicrosoft struct {
model.Base
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
// Azure AD tenant ID — identifies the Azure organization
TenantID string `gorm:"size:255;not null" json:"tenant_id"`
// Azure AD client/object ID for the application
ClientID string `gorm:"size:255;not null" json:"client_id"`
// Microsoft Graph API access token
AccessToken string `gorm:"size:2048;not null" json:"-"`
// OAuth 2.0 refresh token for token renewal
RefreshToken string `gorm:"size:2048" json:"-"`
// Microsoft Teams team ID (optional — for Teams channel integration)
TeamID string `gorm:"size:255" json:"team_id,omitempty"`
// Microsoft Teams channel ID (optional — for specific Teams channel)
ChannelID string `gorm:"size:255" json:"channel_id,omitempty"`
// Display name of the connected Microsoft resource
DisplayName string `gorm:"size:255" json:"display_name,omitempty"`
// Webhook subscription ID for Graph API subscriptions
SubscriptionID string `gorm:"size:255" json:"subscription_id,omitempty"`
// Flag indicating reauthorization is required
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required,omitempty"`
}
func (ChannelMicrosoft) TableName() string { return "channel_microsoft" }
func (c ChannelMicrosoft) GetChannelBase() ChannelBase {
return ChannelBase{
ID: c.ID,
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: "microsoft",
}
}
func (c ChannelMicrosoft) GetChannelType() string { return "microsoft" }
+25
View File
@@ -0,0 +1,25 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelSms represents a generic SMS channel configuration.
// Reference: P2B M2 spec (generic SMS provider)
type ChannelSms struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
PhoneNumber string `gorm:"size:50;not null;index" json:"phone_number"`
Provider string `gorm:"size:50" json:"provider"` // twilio/bandwidth
ProviderConfig string `gorm:"type:jsonb" json:"provider_config"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelSms) TableName() string { return "channel_sms" }
func (c ChannelSms) GetInboxID() uint { return c.InboxID }
func (c ChannelSms) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeSms }
@@ -0,0 +1,80 @@
package channel
// ChannelTelegram represents a Telegram bot channel configuration.
// Reference: Chatwoot app/models/channel/telegram.rb
//
// Chatwoot's ChannelTelegram model attributes:
// - account_id: FK to Account
// - bot_token: Telegram Bot API token (from @BotFather)
// - bot_name: Bot display name (validated via getMe API)
// - welcome_message: Auto-sent to new contacts
// - reauthorization_required: Flag when webhook/bot fails
//
// Chatwoot lifecycle hooks:
// - before_validation :ensure_valid_bot_token → calls getMe API
// - before_save :setup_telegram_webhook → calls setWebhook API
// - after_destroy :delete_telegram_webhook → calls deleteWebhook API
//
// gochat enhancements beyond Chatwoot:
// - webhook_url: Stores the configured webhook URL for verification
// - account_id: Explicit FK (Chatwoot uses polymorphic through Inbox)
// - welcome_message: Moved to channel model (Chatwoot uses inbox settings)
// - reauthorization_required: Tracks when bot needs re-auth
import (
"github.com/gochat/gochat/internal/model"
)
// ChannelTelegram stores Telegram bot configuration for a channel.
type ChannelTelegram struct {
model.Base // ID, CreatedAt, UpdatedAt, DeletedAt (soft delete via GORM)
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"index" json:"inbox_id,omitempty"`
// Bot token obtained from @BotFather (format: N:XXXXX)
// Reference: Chatwoot EDITABLE_ATTRS = [:bot_token]
BotToken string `gorm:"type:varchar(255);not null;unique" json:"bot_token"`
// Bot display name — validated and set via getMe API
// Reference: Chatwoot sets this during ensure_valid_bot_token
BotName string `gorm:"type:varchar(255)" json:"bot_name,omitempty"`
// Webhook URL configured via setWebhook API
// gochat extension: stored for verification/debugging
WebhookURL string `gorm:"type:varchar(512)" json:"webhook_url,omitempty"`
// Welcome message sent to new contacts
// Reference: Chatwoot uses inbox.welcome_message, gochat puts it on the channel model
WelcomeMessage string `gorm:"type:text" json:"welcome_message,omitempty"`
// Flag indicating the bot token needs reauthorization
// Reference: Chatwoot reauthorization_required attribute
// Set when webhook delivery fails or getMe validation fails
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required,omitempty"`
}
// TableName returns the GORM table name for ChannelTelegram.
// GetChannelBase returns a ChannelBase populated from ChannelTelegram fields.
// Required by the Channelable interface.
func (ct *ChannelTelegram) GetChannelBase() ChannelBase {
return ChannelBase{
ID: ct.ID,
AccountID: ct.AccountID,
InboxID: ct.InboxID,
ChannelType: ct.GetChannelType(),
CreatedAt: ct.CreatedAt,
UpdatedAt: ct.UpdatedAt,
DeletedAt: ct.DeletedAt,
}
}
// GetChannelType returns the channel type identifier.
func (ct *ChannelTelegram) GetChannelType() string { return "telegram" }
func (ChannelTelegram) TableName() string {
return "channel_telegrams"
}
@@ -0,0 +1,28 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelTelegram represents a Telegram channel configuration.
// Reference: Chatwoot Channel::Telegram + P2B M2 spec
type ChannelTelegram struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
BotToken string `gorm:"size:255;not null" json:"bot_token"`
BotName string `gorm:"size:255" json:"bot_name"`
ChatID int64 `json:"chat_id"`
ReviewMode bool `gorm:"default:false" json:"review_mode"`
HasSendPermission bool `gorm:"default:false" json:"has_send_permission"`
WelcomeMessage string `gorm:"type:text" json:"welcome_message"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelTelegram) TableName() string { return "channel_telegrams" }
func (c ChannelTelegram) GetInboxID() uint { return c.InboxID }
func (c ChannelTelegram) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeTelegram }
+66
View File
@@ -0,0 +1,66 @@
package channel
// ChannelTikTok represents a TikTok Business channel configuration.
// Reference: TikTok Business API — https://business-api.tiktok.com/portal/auth
//
// GoChat addition (Chatwoot does not have TikTok channel support).
// TikTok Business API provides:
// - OAuth2 authentication flow for connecting TikTok Business accounts
// - Direct messaging API for sending/receiving customer messages
// - Webhook events for incoming messages, read receipts, etc.
//
// Lifecycle:
// - OnCreate: OAuth2 token exchange → store access_token/refresh_token → setup webhook
// - OnReauthorization: refresh expired tokens via TikTok OAuth2 refresh endpoint
// - OnDestroy: delete webhook subscription
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelTikTok stores TikTok Business channel configuration.
type ChannelTikTok struct {
model.Base // ID, CreatedAt, UpdatedAt, DeletedAt
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"index" json:"inbox_id,omitempty"`
// TikTok Business account ID (obtained after OAuth authorization)
TikTokBusinessID string `gorm:"column:tiktok_business_id;type:varchar(255);not null" json:"tiktok_business_id"`
// OAuth2 tokens (obtained via BuildAuthURL → ExchangeToken flow)
AccessToken string `gorm:"type:varchar(512)" json:"access_token,omitempty"`
RefreshToken string `gorm:"type:varchar(512)" json:"refresh_token,omitempty"`
TokenExpiresAt time.Time `gorm:"" json:"token_expires_at,omitempty"`
// Webhook verification token (set during channel creation for webhook setup)
WebhookVerifyToken string `gorm:"type:varchar(255)" json:"webhook_verify_token,omitempty"`
// Flag indicating the channel needs re-authorization (token expired/revoked)
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required,omitempty"`
}
// GetChannelBase returns the common ChannelBase fields.
// For TikTok, we construct a ChannelBase from our fields since we don't embed it directly.
func (c ChannelTikTok) GetChannelBase() ChannelBase {
return ChannelBase{
ID: c.ID,
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: "tiktok",
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
DeletedAt: c.DeletedAt,
}
}
// GetChannelType returns the channel type identifier.
func (c ChannelTikTok) GetChannelType() string { return "tiktok" }
// TableName returns the GORM table name for ChannelTikTok.
func (ChannelTikTok) TableName() string { return "channel_tiktoks" }
@@ -0,0 +1,26 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelTiktok represents a TikTok channel configuration.
// Reference: P2B M2 spec
type ChannelTiktok struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
AccessToken string `gorm:"size:1024" json:"-"`
RefreshToken string `gorm:"size:1024" json:"-"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
TiktokAccountID string `gorm:"size:255;not null;index" json:"tiktok_account_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelTiktok) TableName() string { return "channel_tiktoks" }
func (c ChannelTiktok) GetInboxID() uint { return c.InboxID }
func (c ChannelTiktok) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeTiktok }
+61
View File
@@ -0,0 +1,61 @@
package channel
// ChannelTwilioSMS represents a Twilio SMS channel configuration.
// Reference: Twilio SMS API — https://www.twilio.com/docs/sms
// + Chatwoot app/models/channel/twilio_sms.rb (partial — Chatwoot uses Twilio for SMS + WhatsApp)
//
// GoChat addition — full Twilio SMS integration.
// The Twilio SMS channel connects Twilio phone numbers to gochat conversations.
//
// Key Twilio API concepts:
// - Account SID: Twilio account identifier
// - Auth Token: Twilio authentication token (used for API + webhook verification)
// - Phone Number: the Twilio phone number sending/receiving SMS
// - Messaging Service SID: optional, for grouped phone numbers
// - Webhook: Twilio sends inbound messages to a configured URL
//
// Design notes:
// - Uses GORM soft-delete pattern consistent with other channel models
// - JSON field tags follow snake_case convention per P2D §7
// - Twilio supports both SMS and WhatsApp via the same API — this model
// handles SMS only; WhatsApp has its own ChannelWhatsApp model
// - Sensitive fields (auth_token) are stored in Inbox.ChannelConfig
import "gorm.io/gorm"
// ChannelTwilioSMS is the GORM model for Twilio SMS channels.
// Reference: Chatwoot app/models/channel/twilio_sms.rb
// Design: Follows the same pattern as ChannelTelegram — minimal GORM model with
// config stored in Inbox.ChannelConfig (JSONB).
type ChannelTwilioSMS struct {
gorm.Model
// Twilio Account SID
// Unique identifier for the Twilio subaccount
AccountSID string `json:"account_sid" gorm:"type:varchar(255);not null"`
// Phone number assigned to this channel (Twilio purchased number)
PhoneNumber string `json:"phone_number" gorm:"type:varchar(50);uniqueIndex;not null"`
// Messaging Service SID (optional)
// Used when multiple phone numbers are grouped under a messaging service
MessagingServiceSID string `json:"messaging_service_sid" gorm:"type:varchar(255)"`
// AccountID references the owning gochat account (multi-tenant)
AccountID uint `json:"account_id" gorm:"index;not null"`
// InboxID references the associated inbox (1:1 with channel)
InboxID uint `json:"inbox_id" gorm:"uniqueIndex;not null"`
}
// TableName overrides GORM table name convention.
func (ChannelTwilioSMS) TableName() string { return "channel_twilio_sms" }
// Channelable interface implementation for ChannelTwilioSMS.
func (c *ChannelTwilioSMS) GetChannelType() string { return "twilio_sms" }
func (c *ChannelTwilioSMS) GetChannelID() string { return c.AccountSID }
func (c *ChannelTwilioSMS) GetName() string { return c.PhoneNumber }
func (c *ChannelTwilioSMS) GetAccountID() uint { return c.AccountID }
func (c *ChannelTwilioSMS) GetInboxID() uint { return c.InboxID }
func (c *ChannelTwilioSMS) SetInboxID(id uint) { c.InboxID = id }
@@ -0,0 +1,29 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelTwilioSMS represents a Twilio SMS/WhatsApp channel configuration.
// Reference: Chatwoot Channel::TwilioSms + P2B M2 spec
type ChannelTwilioSMS struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
PhoneNumber string `gorm:"size:50;not null;index" json:"phone_number"`
AccountSID string `gorm:"size:255;not null" json:"account_sid"`
AuthToken string `gorm:"size:255;not null" json:"-"`
APIKey string `gorm:"size:255" json:"api_key"`
APISecret string `gorm:"size:255" json:"-"`
MessagingServiceSID string `gorm:"size:255" json:"messaging_service_sid"`
Medium string `gorm:"size:50;default:'sms'" json:"medium"` // sms/whatsapp
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelTwilioSMS) TableName() string { return "channel_twilio_sms" }
func (c ChannelTwilioSMS) GetInboxID() uint { return c.InboxID }
func (c ChannelTwilioSMS) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeTwilioSms }
+80
View File
@@ -0,0 +1,80 @@
package channel
import "github.com/gochat/gochat/internal/model"
// ChannelTwitter represents a Twitter/X channel.
// Reference: Chatwoot app/models/channel/twitter_profile.rb
//
// Chatwoot's ChannelTwitter (Channel::TwitterProfile) model attributes:
// - account_id: FK to Account
// - twitter_access_token: OAuth access token for Twitter API
// - twitter_access_token_secret: OAuth access token secret
// - profile_id: Twitter user ID (numeric)
// - name: Twitter display name
// - screen_name: Twitter @handle
// - profile_image_url: Twitter avatar URL
// - tweets_enabled: whether tweet webhooks should create conversations/messages
// - reauthorization_required: Flag when token needs refresh
//
// gochat enhancements beyond Chatwoot:
// - webhook_id: Twitter webhook ID for CRC/subscription management
// - webhook_env: Twitter Account Activity API environment name
// - uses OAuth 2.0 with PKCE for newer Twitter API v2 integration
type ChannelTwitter struct {
model.Base
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
// Twitter user ID (numeric string) — identifies the Twitter account
TwitterUserID string `gorm:"size:255;not null;uniqueIndex" json:"twitter_user_id"`
// Twitter display name
Name string `gorm:"size:255" json:"name,omitempty"`
// Twitter @handle (screen_name)
ScreenName string `gorm:"size:255" json:"screen_name,omitempty"`
// Twitter avatar URL
ProfileImageURL string `gorm:"size:512" json:"profile_image_url,omitempty"`
// OAuth 2.0 access token for Twitter API v2
AccessToken string `gorm:"size:1024;not null" json:"-"`
// OAuth 2.0 refresh token for token renewal
RefreshToken string `gorm:"size:1024" json:"-"`
// OAuth 1.0a access token (legacy Twitter API compatibility)
TwitterAccessToken string `gorm:"size:512;column:twitter_access_token" json:"-"`
// OAuth 1.0a access token secret (legacy)
TwitterAccessTokenSecret string `gorm:"size:512;column:twitter_access_token_secret" json:"-"`
// Twitter Account Activity API webhook ID
WebhookID string `gorm:"size:255" json:"webhook_id,omitempty"`
// Twitter Account Activity API environment name
WebhookEnv string `gorm:"size:255" json:"webhook_env,omitempty"`
// Whether incoming tweets should be processed into conversations/messages
TweetsEnabled bool `gorm:"default:true" json:"tweets_enabled"`
// Flag indicating reauthorization is required (token expired/revoked)
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required,omitempty"`
}
func (ChannelTwitter) TableName() string { return "channel_twitter_profiles" }
func (c ChannelTwitter) GetChannelBase() ChannelBase {
return ChannelBase{
ID: c.ID,
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: "twitter",
}
}
func (c ChannelTwitter) GetChannelType() string { return "twitter" }
@@ -0,0 +1,19 @@
package channel
import "github.com/gochat/gochat/internal/model"
// ChannelWebWidget represents a web widget channel configuration.
// Reference: Chatwoot app/models/channel/web_widget.rb
type ChannelWebWidget struct {
model.BaseModelWithoutID
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
WebsiteURL string `gorm:"size:1024;not null" json:"website_url"`
WelcomeTitle string `gorm:"size:255" json:"welcome_title"`
WelcomeTagline string `gorm:"size:255" json:"welcome_tagline"`
WidgetColor string `gorm:"size:20;default:#1f93ff" json:"widget_color"`
ReplyTime string `gorm:"size:50;default:in_a_few_minutes" json:"reply_time"`
HmacToken string `gorm:"size:255" json:"hmac_token,omitempty"`
PreChatFormEnabled bool `gorm:"default:false" json:"pre_chat_form_enabled"`
}
func (ChannelWebWidget) TableName() string { return "channel_web_widgets" }
@@ -0,0 +1,37 @@
package channel
import (
"encoding/json"
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelWebWidget represents a web widget channel configuration.
// Reference: Chatwoot Channel::WebWidget + P2B M2 spec
type ChannelWebWidget struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
WebsiteToken string `gorm:"uniqueIndex;size:255;not null" json:"website_token"`
WebsiteURL string `gorm:"size:512" json:"website_url"`
WelcomeHeading string `gorm:"size:255" json:"welcome_heading"`
WelcomeTagline string `gorm:"size:255" json:"welcome_tagline"`
PreChatFormEnabled bool `gorm:"default:false" json:"pre_chat_form_enabled"`
PreChatFormMessage string `gorm:"type:text" json:"pre_chat_form_message"`
PreChatFormOptions json.RawMessage `gorm:"type:jsonb" json:"pre_chat_form_options"`
HMACToken string `gorm:"size:255;not null" json:"hmac_token"`
ReplyTime string `gorm:"size:50;default:'in_a_few_minutes'" json:"reply_time"` // in_a_few_minutes/in_a_few_hours/in_a_day
ContinuousPolyfill bool `gorm:"default:false" json:"continuous_polyfill"`
FeatureFlags json.RawMessage `gorm:"type:jsonb" json:"feature_flags"`
ReferrerHost string `gorm:"size:255" json:"referrer_host"`
WidgetColor string `gorm:"size:50" json:"widget_color"`
AvatarURL string `gorm:"size:512" json:"avatar_url"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelWebWidget) TableName() string { return "channel_web_widgets" }
func (c ChannelWebWidget) GetInboxID() uint { return c.InboxID }
func (c ChannelWebWidget) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeWebWidget }
+122
View File
@@ -0,0 +1,122 @@
package channel
import (
"github.com/gochat/gochat/internal/model"
)
// ChannelWhatsApp represents a WhatsApp Business channel configuration.
// Reference: Chatwoot app/models/channel/whatsapp.rb
//
// Chatwoot's ChannelWhatsApp (Channel::Whatsapp) model attributes:
// - account_id: FK to Account
// - phone_number: WhatsApp phone number in international format
// - phone_number_id: WhatsApp Business Phone Number ID (from Meta Business Manager)
// - business_account_id: WhatsApp Business Account ID (WABA ID)
// - provider: WhatsApp provider backend (whatsapp_cloud or 360dialog)
// - message_templates: Cached WhatsApp message templates JSON
// - template_namespace: WhatsApp template namespace (deprecated, retained for migration)
// - provider_config: Provider-specific configuration JSON
// - reauthorization_required: Flag when token/API connection fails
//
// Chatwoot lifecycle hooks:
// - before_create :validate_provider_config (checks provider-specific setup)
// - after_create :setup_webhook (registers webhook with provider)
// - after_destroy :delete_webhook (removes webhook registration)
//
// gochat enhancements beyond Chatwoot:
// - webhook_verify_token: Token for WhatsApp webhook verification (Cloud API hub.mode=subscribe)
// - auto_create_contact: Whether to auto-create contacts from incoming messages
// - provider_config stored as typed JSON rather than serialized hash
//
// WhatsApp supports two provider backends:
// - whatsapp_cloud: Meta WhatsApp Business Cloud API (https://developers.facebook.com/docs/whatsapp/cloud-api)
// - 360dialog: 360dialog WhatsApp Business API (https://docs.360dialog.com/docs/whatsapp-api)
type ChannelWhatsApp struct {
model.Base // ID, CreatedAt, UpdatedAt, DeletedAt (soft delete via GORM)
// Account foreign key
AccountID uint `gorm:"index;not null" json:"account_id"`
// Inbox foreign key — links to the Inbox that wraps this channel
InboxID uint `gorm:"not null;uniqueIndex" json:"inbox_id"`
// WhatsApp phone number in international format (e.g., "+1234567890")
PhoneNumber string `gorm:"size:50;not null;index" json:"phone_number"`
// WhatsApp Business Phone Number ID — from Meta Business Manager or 360dialog dashboard
// Used as the sender identifier in Cloud API calls
PhoneNumberID string `gorm:"size:255" json:"phone_number_id"`
// WhatsApp Business Account ID (WABA ID) — identifies the WhatsApp Business Account
BusinessAccountID string `gorm:"size:255" json:"business_account_id"`
// Display name of the WhatsApp Business account
WhatsAppAccountName string `gorm:"size:255" json:"whatsapp_account_name"`
// Access token for WhatsApp Business API (Cloud API or 360dialog API)
// For Cloud API: permanent access token from Meta App dashboard
// For 360dialog: API key from 360dialog hub
AccessToken string `gorm:"size:512;not null" json:"access_token"`
// WhatsApp provider backend — determines which API to use
// Reference: model.WhatsAppProvider enum (whatsapp_cloud / 360dialog)
Provider string `gorm:"size:50" json:"provider"`
// Cached WhatsApp message templates (JSON blob)
// Reference: Chatwoot caches templates locally for quick lookup
MessageTemplates string `gorm:"type:text" json:"message_templates"`
// Template namespace (deprecated by WhatsApp, retained for backward compatibility)
TemplateNamespace string `gorm:"size:255" json:"template_namespace"`
// Provider-specific configuration (JSON blob)
// For 360dialog: { "api_key": "...", "namespace": "..." }
// For Cloud API: { "app_id": "...", "webhook_verify_token": "..." }
ProviderConfig string `gorm:"type:text" json:"provider_config"`
// Webhook verify token — used during WhatsApp webhook subscription verification
// Reference: Cloud API GET webhook with hub.mode=subscribe, hub.verify_token
WebhookVerifyToken string `gorm:"size:255" json:"webhook_verify_token"`
// Whether to auto-create contacts from incoming WhatsApp messages
AutoCreateContact bool `gorm:"default:false" json:"auto_create_contact"`
// Flag indicating the channel needs re-authorization (token expired or API failure)
ReauthorizationRequired bool `gorm:"default:false" json:"reauthorization_required"`
// Inbox relationship
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
// TableName returns the GORM table name for ChannelWhatsApp.
func (ChannelWhatsApp) TableName() string { return "channel_whatsapps" }
// GetChannelBase returns a ChannelBase populated from ChannelWhatsApp fields.
// Required by the Channelable interface.
func (c *ChannelWhatsApp) GetChannelBase() ChannelBase {
return ChannelBase{
ID: c.ID,
AccountID: c.AccountID,
InboxID: c.InboxID,
ChannelType: c.GetChannelType(),
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
DeletedAt: c.DeletedAt,
}
}
// GetChannelType returns the channel type identifier.
func (c *ChannelWhatsApp) GetChannelType() string { return "whatsapp" }
// GetInboxID returns the inbox ID for this channel.
func (c *ChannelWhatsApp) GetInboxID() uint { return c.InboxID }
// IsCloudAPI returns true if this channel uses the WhatsApp Cloud API provider.
func (c *ChannelWhatsApp) IsCloudAPI() bool {
return c.Provider == "whatsapp_cloud"
}
// Is360Dialog returns true if this channel uses the 360dialog provider.
func (c *ChannelWhatsApp) Is360Dialog() bool {
return c.Provider == "360dialog"
}
@@ -0,0 +1,31 @@
package channel
import (
"time"
"github.com/gochat/gochat/internal/model"
)
// ChannelWhatsapp represents a WhatsApp channel configuration.
// Reference: Chatwoot Channel::Whatsapp + P2B M2 spec
type ChannelWhatsapp struct {
ID uint `gorm:"primaryKey" json:"id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
PhoneNumber string `gorm:"size:50;not null;index" json:"phone_number"`
PhoneNumberID string `gorm:"size:255" json:"phone_number_id"`
BusinessAccountID string `gorm:"size:255" json:"business_account_id"`
WABAID string `gorm:"size:255" json:"waba_id"`
Provider string `gorm:"size:50" json:"provider"` // whatsapp_cloud/360dialog
MessageTemplates string `gorm:"type:text" json:"message_templates"`
TemplateNamespace string `gorm:"size:255" json:"template_namespace"`
ProviderConfig string `gorm:"type:jsonb" json:"provider_config"`
AutoCreateContact bool `gorm:"default:false" json:"auto_create_contact"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Inbox model.Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
}
func (ChannelWhatsapp) TableName() string { return "channel_whatsapps" }
func (c ChannelWhatsapp) GetInboxID() uint { return c.InboxID }
func (c ChannelWhatsapp) GetChannelType() model.InboxChannelType { return model.InboxChannelTypeWhatsapp }