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:
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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" }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user