Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fc3fc3656 | |||
| b99360e180 | |||
| 109ca703ef | |||
| 794746e20d | |||
| 85848c2d6c | |||
| 86d4681fcd | |||
| 0a14a29ac6 | |||
| df07b4b672 | |||
| 7bd151ad62 | |||
| ddc867ad06 | |||
| a111f5b5e9 | |||
| 1897805483 | |||
| 8919db6fc3 | |||
| d69b26af51 | |||
| a8dae85f42 | |||
| d3bc62f810 | |||
| 13683adfba | |||
| e183b62aba | |||
| ce9d72b50d | |||
| 92358a673b | |||
| 99308cd7a4 | |||
| 561ba24090 | |||
| 3e1cb9a500 | |||
| 5ae8e3e001 | |||
| faea3637e0 | |||
| 79eaa5217d | |||
| 0e0891f1a8 | |||
| 33aaf666ae | |||
| a634938799 | |||
| 14ff4ab2f1 | |||
| b3de21e7d1 | |||
| 72a373b17c | |||
| bc290f3bed | |||
| a4704ad267 | |||
| d1762ad5df | |||
| df1467c518 | |||
| 7cdd97ca59 | |||
| 5b36d10b04 | |||
| 56699fdd4d | |||
| fcf1cc41c8 | |||
| b8640ccb1d | |||
| ca06298e64 |
@@ -1,66 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/mokocli/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/mokocli/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
@@ -27,7 +27,7 @@ name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
@@ -52,7 +52,7 @@ on:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
@@ -66,7 +66,6 @@ jobs:
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
@@ -102,7 +101,7 @@ jobs:
|
||||
php ${MOKO_CLI}/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
@@ -121,7 +120,7 @@ jobs:
|
||||
|
||||
- name: Update RC release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
@@ -269,7 +268,7 @@ jobs:
|
||||
!startsWith(steps.platform.outputs.platform, 'joomla')
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
SEMVER_TAG="v${VERSION}"
|
||||
|
||||
@@ -294,7 +293,7 @@ jobs:
|
||||
|
||||
- name: Update release notes and promote changelog
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Get the stable release info (version and ID)
|
||||
@@ -363,7 +362,7 @@ jobs:
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
@@ -392,7 +391,7 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
@@ -416,7 +415,7 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
@@ -437,7 +436,7 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
@@ -463,5 +462,5 @@ jobs:
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
@@ -13,12 +13,6 @@
|
||||
name: "Generic: Project CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
|
||||
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
|
||||
|
||||
name: "Universal: CI Issue Reporter"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
gate:
|
||||
description: "CI gate name (e.g. PR Validation, Repository Health)"
|
||||
required: true
|
||||
type: string
|
||||
details:
|
||||
description: "Human-readable failure description"
|
||||
required: true
|
||||
type: string
|
||||
severity:
|
||||
description: "error or warning"
|
||||
required: false
|
||||
type: string
|
||||
default: "error"
|
||||
workflow:
|
||||
description: "Workflow name for the issue title"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
secrets:
|
||||
MOKOGITEA_TOKEN:
|
||||
required: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
report:
|
||||
name: "Report: ${{ inputs.gate }}"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone MokoCLI
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
||||
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
||||
|
||||
- name: Report CI failure
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
run: |
|
||||
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
|
||||
/tmp/mokocli/cli/ci_issue_reporter.sh \
|
||||
--gate "${{ inputs.gate }}" \
|
||||
--details "${{ inputs.details }}" \
|
||||
--severity "${{ inputs.severity }}" \
|
||||
--workflow "${{ inputs.workflow }}"
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
@@ -33,17 +33,17 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
token: ${{ secrets.GA_TOKEN }}
|
||||
|
||||
- name: Delete merged branches
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Merged Branch Cleanup ==="
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
|
||||
# List branches via API
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||
|
||||
DELETED=0
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Check if branch is merged into main
|
||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||
echo " Deleting merged branch: ${BRANCH}"
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
fi
|
||||
@@ -66,20 +66,20 @@ jobs:
|
||||
|
||||
- name: Clean old workflow runs
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Workflow Run Cleanup ==="
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Get old completed runs
|
||||
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/actions/runs?status=completed&limit=50" | \
|
||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||
|
||||
DELETED=0
|
||||
for RUN_ID in $RUNS; do
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
done
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Deploy
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||
# VERSION: 04.07.00
|
||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||
|
||||
name: "Universal: Deploy to Dev (Manual)"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all remote files before uploading'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: SFTP Deploy to Dev
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
php -v && composer --version
|
||||
|
||||
- name: Setup MokoStandards tools
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||
/tmp/mokostandards-api 2>/dev/null || true
|
||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Check FTP configuration
|
||||
id: check
|
||||
env:
|
||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
run: |
|
||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
REMOTE="${PATH_VAR%/}"
|
||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
[ -z "$PORT" ] && PORT="22"
|
||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
run: |
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||
> /tmp/sftp-config.json
|
||||
|
||||
if [ -n "$SFTP_KEY" ]; then
|
||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 01.08.18
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
@@ -19,7 +19,7 @@ permissions:
|
||||
issues: write
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
create-branch:
|
||||
@@ -28,8 +28,8 @@ jobs:
|
||||
steps:
|
||||
- name: Create branch and comment
|
||||
run: |
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "Created branch: ${BRANCH}"
|
||||
|
||||
# Comment on issue with branch link
|
||||
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
|
||||
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||
|
||||
curl -sf -X POST \
|
||||
|
||||
+534
-521
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.02.00
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
@@ -59,11 +59,6 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.ref_name }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Update submodules to main
|
||||
run: |
|
||||
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
||||
|
||||
- name: Setup mokocli tools
|
||||
env:
|
||||
|
||||
@@ -29,20 +29,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
REPO: ${{ github.repository }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
|
||||
if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then
|
||||
echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1
|
||||
fi
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
SUFFIX="${BRANCH#rc/}"
|
||||
DEV_BRANCH="dev/${SUFFIX}"
|
||||
API="${GITEA_URL}/api/v1/repos/${REPO}/branches"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Create dev/ branch from rc/ branch
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||
@@ -50,22 +42,25 @@ jobs:
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||
"${API}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "201" ]; then
|
||||
echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1
|
||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection)
|
||||
ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));')
|
||||
# Delete rc/ branch
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||
fi
|
||||
|
||||
echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
name: "Universal: Workflow Sync Trigger"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
@@ -27,9 +26,8 @@ jobs:
|
||||
name: Sync workflows to live repos
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true &&
|
||||
!contains(github.event.pull_request.title, '[skip sync]'))
|
||||
github.event.pull_request.merged == true &&
|
||||
!contains(github.event.pull_request.title, '[skip sync]')
|
||||
|
||||
steps:
|
||||
- name: Determine platform from repo name
|
||||
@@ -51,14 +49,8 @@ jobs:
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||
|
||||
- name: Install PHP
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
|
||||
fi
|
||||
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
+41
-2
@@ -1,9 +1,26 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
## [01.08.00] --- 2026-06-23
|
||||
### Added
|
||||
- **Instagram carousel**: Multi-image/video posts via Meta carousel container flow (up to 10 items)
|
||||
- **Instagram Reels**: Short-form video publishing via REELS media type
|
||||
- **Instagram Stories**: Image and video story publishing via STORIES media type
|
||||
- **Instagram alt text**: Alt text support for image containers
|
||||
- **Nostr plugin**: Full NIP-01 WebSocket relay publishing with BIP-340 Schnorr signatures (pure PHP, requires ext-gmp)
|
||||
- **Nostr**: Publishes kind-1 text note events to multiple relays with automatic failover
|
||||
- **Nostr**: Raw WebSocket client using stream_socket_client (no external dependencies)
|
||||
- **Nostr**: Public key derivation and event signing via secp256k1 elliptic curve math
|
||||
- **Threads carousel**: Support up to 20-item carousel posts via Threads API multi-container flow
|
||||
- **Threads polls**: Poll creation support via poll_options parameter (2-4 options)
|
||||
- **Threads spoiler tags**: Content warning / spoiler flag support for Threads posts
|
||||
- **Threads text-only optimization**: Simplified single-step flow for text-only posts without media
|
||||
- **Facebook Reels**: Publish video Reels via Graph API video_reels endpoint (#162)
|
||||
- **Facebook Stories**: Publish image and video Stories via photo_stories/video_stories endpoints (#162)
|
||||
- **Facebook scheduled posts**: Schedule feed posts with scheduled_publish_time parameter (#162)
|
||||
- **Facebook draft posts**: Save feed posts as unpublished drafts (#162)
|
||||
|
||||
## [01.08.00] --- 2026-06-23
|
||||
### Fixed
|
||||
- Webservices plugin Joomla 6 compatibility — `onBeforeApiRoute` receives `BeforeApiRouteEvent` object, extract router via `$event->getRouter()`
|
||||
|
||||
## [01.07.00] --- 2026-06-23
|
||||
|
||||
@@ -16,6 +33,7 @@
|
||||
|
||||
### Fixed
|
||||
- **License warning**: Removed duplicate from system plugin (install script already shows it)
|
||||
- **Content plugin**: Fixed func_get_arg crash when non-article content is saved (e.g. update sites, installer)
|
||||
|
||||
## [01.05.00] --- 2026-06-23
|
||||
|
||||
@@ -52,3 +70,24 @@
|
||||
- **Bluesky**: Replaced md5() with hash('sha256', ...) for cache key
|
||||
- **ServiceController**: Exception details no longer exposed to client
|
||||
- **License warning**: Removed duplicate from system plugin -- install script already shows it with direct edit link
|
||||
|
||||
## [01.04.01] --- 2026-06-21
|
||||
|
||||
|
||||
## [01.04.01] --- 2026-06-21
|
||||
|
||||
|
||||
## [01.04.00] --- 2026-06-21
|
||||
|
||||
### Fixed
|
||||
- **Package manifest**: Added missing `plg_system_mokosuitecross_events` and `plg_system_mokosuitecross_gallery` to `pkg_mokosuitecross.xml` — these system plugins were not installed with the package
|
||||
- **Cleanup**: Removed old `src/` directory (pre-rename cruft with `mokojoomcross` files)
|
||||
|
||||
## [01.03.00] --- 2026-06-21
|
||||
|
||||
|
||||
<!-- VERSION: 01.08.18 -->
|
||||
|
||||
All notable changes to MokoSuiteCross will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoSuiteCross
|
||||
|
||||
<!-- VERSION: 01.08.00 -->
|
||||
<!-- VERSION: 01.08.18 -->
|
||||
|
||||
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
PKG_MOKOSUITECROSS="MokoSuiteCross"
|
||||
PKG_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms. Automatically publish articles to Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, and Slack."
|
||||
PKG_MOKOSUITECROSS_DESCRIPTION="Cross-post Joomla articles to 38 platforms including Facebook, Instagram, X/Twitter, LinkedIn, Threads, Mastodon, Bluesky, Nostr, TikTok, YouTube, Pinterest, Reddit, Medium, Telegram, Discord, Slack, Teams, Mailchimp, SendGrid, Brevo, and more. Features scheduled posting, template placeholders, UTM tagging, link shortening, caption rotation, and per-article service selection."
|
||||
PKG_MOKOSUITECROSS_PHP_VERSION_ERROR="MokoSuiteCross requires PHP %s or later."
|
||||
PKG_MOKOSUITECROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoSuiteCross → Dashboard to migrate your settings."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="component" method="upgrade">
|
||||
<name>com_mokosuitecross</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.05 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.07 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.08 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.09 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.10 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.11 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.12 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.13 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.14 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.15 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.16 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.17 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.08.18 — no schema changes */
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteCross</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
+13
-42
@@ -270,25 +270,11 @@ XML;
|
||||
|
||||
/**
|
||||
* Add cross-post status badges before article content in admin.
|
||||
*
|
||||
* Joomla 5/6 compatible — accepts both BeforeDisplayEvent and legacy parameters.
|
||||
*/
|
||||
public function onContentBeforeDisplay($event): string
|
||||
public function onContentBeforeDisplay(\Joomla\CMS\Event\Content\BeforeDisplayEvent $event): string
|
||||
{
|
||||
// Joomla 5/6 compatibility
|
||||
if ($event instanceof \Joomla\CMS\Event\Content\BeforeDisplayEvent) {
|
||||
$context = $event->getContext();
|
||||
$article = $event->getItem();
|
||||
} elseif (is_string($event)) {
|
||||
$context = $event;
|
||||
$article = func_get_arg(1);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($context !== 'com_content.article') {
|
||||
return '';
|
||||
}
|
||||
$context = $event->getContext();
|
||||
$article = $event->getItem();
|
||||
|
||||
$app = $this->getApplication();
|
||||
|
||||
@@ -330,26 +316,18 @@ XML;
|
||||
|
||||
/**
|
||||
* Dispatch cross-post when an article is saved and published.
|
||||
*
|
||||
* Joomla 5/6 compatible — accepts both AfterSaveEvent and legacy parameters.
|
||||
*/
|
||||
public function onContentAfterSave($event): void
|
||||
public function onContentAfterSave(\Joomla\CMS\Event\Content\AfterSaveEvent $event): void
|
||||
{
|
||||
// Joomla 5/6 compatibility
|
||||
if ($event instanceof \Joomla\CMS\Event\Content\AfterSaveEvent) {
|
||||
$context = $event->getContext();
|
||||
$article = $event->getItem();
|
||||
$isNew = $event->getIsNew();
|
||||
} else {
|
||||
$context = $event;
|
||||
$article = func_get_arg(1);
|
||||
$isNew = func_get_arg(2);
|
||||
}
|
||||
$context = $event->getContext();
|
||||
|
||||
if ($context !== 'com_content.article') {
|
||||
return;
|
||||
}
|
||||
|
||||
$article = $event->getItem();
|
||||
$isNew = $event->getIsNew();
|
||||
|
||||
if ((int) ($article->state ?? 0) !== 1) {
|
||||
return;
|
||||
}
|
||||
@@ -375,25 +353,18 @@ XML;
|
||||
|
||||
/**
|
||||
* Dispatch cross-post when article state changes to published.
|
||||
*
|
||||
* Joomla 5/6 compatible — accepts both ContentChangeStateEvent and legacy parameters.
|
||||
*/
|
||||
public function onContentChangeState($event): void
|
||||
public function onContentChangeState(\Joomla\CMS\Event\Content\ContentChangeStateEvent $event): void
|
||||
{
|
||||
if ($event instanceof \Joomla\CMS\Event\Content\ContentChangeStateEvent) {
|
||||
$context = $event->getContext();
|
||||
$pks = $event->getPks();
|
||||
$value = $event->getValue();
|
||||
} else {
|
||||
$context = $event;
|
||||
$pks = func_get_arg(1);
|
||||
$value = func_get_arg(2);
|
||||
}
|
||||
$context = $event->getContext();
|
||||
|
||||
if ($context !== 'com_content.article') {
|
||||
return;
|
||||
}
|
||||
|
||||
$pks = $event->getPks();
|
||||
$value = $event->getValue();
|
||||
|
||||
$params = ComponentHelper::getParams('com_mokosuitecross');
|
||||
|
||||
// Unpublish/trash: delete from platforms if configured
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - ActivityPub (Fediverse)</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Google Blogger</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Bluesky</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Brevo (Sendinblue)</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Constant Contact</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - ConvertKit</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Dev.to</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Discord</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Facebook / Meta</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -18,22 +18,10 @@ use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteIn
|
||||
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
/**
|
||||
* Facebook/Meta service plugin for MokoSuiteCross.
|
||||
*
|
||||
* Supports two modes:
|
||||
* 1. Default MokoSuite App — pre-configured app credentials (hidden from admin UI)
|
||||
* 2. Custom App — user provides their own Facebook App ID and Page Access Token
|
||||
*
|
||||
* Credentials format:
|
||||
* {
|
||||
* "mode": "default" | "custom",
|
||||
* "page_access_token": "...", // Only for custom mode
|
||||
* "page_id": "..." // Required — Facebook Page ID
|
||||
* }
|
||||
*/
|
||||
class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface
|
||||
{
|
||||
private const VIDEO_EXTENSIONS = ['mp4', 'mov', 'avi', 'wmv', 'webm'];
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -65,18 +53,84 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuit
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing token or page_id']];
|
||||
}
|
||||
|
||||
$contentType = $params['content_type'] ?? 'feed';
|
||||
|
||||
return match ($contentType) {
|
||||
'reel' => $this->publishReel($message, $media, $token, $pageId, $params),
|
||||
'story' => $this->publishStory($media, $token, $pageId),
|
||||
default => $this->publishFeed($message, $token, $pageId, $params),
|
||||
};
|
||||
}
|
||||
|
||||
private function publishFeed(string $message, string $token, string $pageId, array $params): array
|
||||
{
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/feed';
|
||||
|
||||
$postData = [
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
// Attach link if provided in params
|
||||
if (!empty($params['link'])) {
|
||||
$postData['link'] = $params['link'];
|
||||
}
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
if (!empty($params['scheduled_at'])) {
|
||||
$timestamp = is_numeric($params['scheduled_at'])
|
||||
? (int) $params['scheduled_at']
|
||||
: strtotime($params['scheduled_at']);
|
||||
|
||||
if ($timestamp !== false && $timestamp > 0) {
|
||||
$postData['scheduled_publish_time'] = $timestamp;
|
||||
$postData['published'] = 'false';
|
||||
}
|
||||
} elseif (!empty($params['draft'])) {
|
||||
$postData['published'] = 'false';
|
||||
}
|
||||
|
||||
return $this->apiPost($apiUrl, $postData, $token);
|
||||
}
|
||||
|
||||
private function publishReel(string $message, array $media, string $token, string $pageId, array $params): array
|
||||
{
|
||||
if (empty($media[0])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Reel requires a video URL']];
|
||||
}
|
||||
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/video_reels';
|
||||
|
||||
$postData = [
|
||||
'upload_phase' => 'finish',
|
||||
'video_url' => $media[0],
|
||||
'description' => $message,
|
||||
];
|
||||
|
||||
return $this->apiPost($apiUrl, $postData, $token);
|
||||
}
|
||||
|
||||
private function publishStory(array $media, string $token, string $pageId): array
|
||||
{
|
||||
if (empty($media[0])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Story requires a media URL']];
|
||||
}
|
||||
|
||||
$mediaUrl = $media[0];
|
||||
$extension = strtolower(pathinfo(parse_url($mediaUrl, PHP_URL_PATH) ?: $mediaUrl, PATHINFO_EXTENSION));
|
||||
$isVideo = in_array($extension, self::VIDEO_EXTENSIONS, true);
|
||||
|
||||
if ($isVideo) {
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/video_stories';
|
||||
$postData = ['video_url' => $mediaUrl];
|
||||
} else {
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/photo_stories';
|
||||
$postData = ['photo_url' => $mediaUrl];
|
||||
}
|
||||
|
||||
return $this->apiPost($apiUrl, $postData, $token);
|
||||
}
|
||||
|
||||
private function apiPost(string $url, array $postData, string $token): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($postData),
|
||||
@@ -88,20 +142,18 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuit
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
||||
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true) ?: [];
|
||||
|
||||
if ($httpCode === 200 && !empty($data['id'])) {
|
||||
if ($httpCode >= 200 && $httpCode < 300 && !empty($data['id'])) {
|
||||
return ['success' => true, 'platform_post_id' => $data['id'], 'response' => $data];
|
||||
}
|
||||
|
||||
@@ -143,7 +195,7 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuit
|
||||
|
||||
public function getMaxLength(): int
|
||||
{
|
||||
return 0; // No practical limit
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function supportsMedia(): bool
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Ghost</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Google Business Profile</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Google Chat</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Hashnode</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Instagram</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -20,7 +20,7 @@ use Joomla\Event\SubscriberInterface;
|
||||
/**
|
||||
* Instagram service plugin for MokoSuiteCross.
|
||||
*
|
||||
* Uses the Meta Content Publishing API — a 2-step flow:
|
||||
* Uses the Meta Content Publishing API -- a 2-step flow:
|
||||
* 1. Create a media container via POST /{ig_user_id}/media
|
||||
* 2. Publish the container via POST /{ig_user_id}/media_publish
|
||||
*/
|
||||
@@ -50,24 +50,128 @@ class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSui
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or Instagram account ID.']];
|
||||
}
|
||||
|
||||
// Step 1: Create media container
|
||||
$containerUrl = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media';
|
||||
$containerData = [
|
||||
'caption' => mb_substr($message, 0, 2200),
|
||||
'access_token' => $token,
|
||||
];
|
||||
|
||||
// Attach image if provided
|
||||
if (!empty($media[0])) {
|
||||
$containerData['image_url'] = $media[0];
|
||||
} else {
|
||||
if (empty($media)) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Instagram requires at least one image or video.']];
|
||||
}
|
||||
|
||||
$ch = curl_init($containerUrl);
|
||||
$caption = mb_substr($message, 0, 2200);
|
||||
$mediaType = $params['media_type'] ?? '';
|
||||
$altText = $params['alt_text'] ?? '';
|
||||
|
||||
if ($mediaType === 'reels') {
|
||||
return $this->publishReels($accountId, $token, $caption, $media[0]);
|
||||
}
|
||||
|
||||
if ($mediaType === 'stories') {
|
||||
return $this->publishStories($accountId, $token, $media[0]);
|
||||
}
|
||||
|
||||
if (\count($media) > 1) {
|
||||
return $this->publishCarousel($accountId, $token, $caption, $media, $altText);
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'caption' => $caption,
|
||||
'image_url' => $media[0],
|
||||
];
|
||||
|
||||
if ($altText !== '') {
|
||||
$fields['alt_text'] = $altText;
|
||||
}
|
||||
|
||||
$container = $this->createContainer($accountId, $token, $fields);
|
||||
|
||||
if (!$container['success']) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $container['data'] ?? ['error' => $container['error']]];
|
||||
}
|
||||
|
||||
return $this->publishContainer($accountId, $token, $container['id']);
|
||||
}
|
||||
|
||||
private function publishCarousel(string $accountId, string $token, string $caption, array $media, string $altText): array
|
||||
{
|
||||
$media = \array_slice($media, 0, 10);
|
||||
$childIds = [];
|
||||
|
||||
foreach ($media as $url) {
|
||||
$fields = ['is_carousel_item' => 'true'];
|
||||
|
||||
if ($this->isVideoUrl($url)) {
|
||||
$fields['video_url'] = $url;
|
||||
} else {
|
||||
$fields['image_url'] = $url;
|
||||
|
||||
if ($altText !== '') {
|
||||
$fields['alt_text'] = $altText;
|
||||
}
|
||||
}
|
||||
|
||||
$child = $this->createContainer($accountId, $token, $fields);
|
||||
|
||||
if (!$child['success']) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $child['data'] ?? ['error' => $child['error']]];
|
||||
}
|
||||
|
||||
$childIds[] = $child['id'];
|
||||
}
|
||||
|
||||
$carousel = $this->createContainer($accountId, $token, [
|
||||
'media_type' => 'CAROUSEL',
|
||||
'caption' => $caption,
|
||||
'children' => implode(',', $childIds),
|
||||
]);
|
||||
|
||||
if (!$carousel['success']) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $carousel['data'] ?? ['error' => $carousel['error']]];
|
||||
}
|
||||
|
||||
return $this->publishContainer($accountId, $token, $carousel['id']);
|
||||
}
|
||||
|
||||
private function publishReels(string $accountId, string $token, string $caption, string $videoUrl): array
|
||||
{
|
||||
$container = $this->createContainer($accountId, $token, [
|
||||
'media_type' => 'REELS',
|
||||
'video_url' => $videoUrl,
|
||||
'caption' => $caption,
|
||||
]);
|
||||
|
||||
if (!$container['success']) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $container['data'] ?? ['error' => $container['error']]];
|
||||
}
|
||||
|
||||
return $this->publishContainer($accountId, $token, $container['id']);
|
||||
}
|
||||
|
||||
private function publishStories(string $accountId, string $token, string $mediaUrl): array
|
||||
{
|
||||
$fields = ['media_type' => 'STORIES'];
|
||||
|
||||
if ($this->isVideoUrl($mediaUrl)) {
|
||||
$fields['video_url'] = $mediaUrl;
|
||||
} else {
|
||||
$fields['image_url'] = $mediaUrl;
|
||||
}
|
||||
|
||||
$container = $this->createContainer($accountId, $token, $fields);
|
||||
|
||||
if (!$container['success']) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $container['data'] ?? ['error' => $container['error']]];
|
||||
}
|
||||
|
||||
return $this->publishContainer($accountId, $token, $container['id']);
|
||||
}
|
||||
|
||||
private function createContainer(string $accountId, string $token, array $fields): array
|
||||
{
|
||||
$url = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media';
|
||||
|
||||
$fields['access_token'] = $token;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($containerData),
|
||||
CURLOPT_POSTFIELDS => http_build_query($fields),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
@@ -75,36 +179,34 @@ class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSui
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
||||
|
||||
return ['success' => false, 'error' => 'Connection error: ' . $curlError];
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true) ?: [];
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300 || empty($data['id'])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
return ['success' => false, 'error' => $data['error']['message'] ?? 'Container creation failed', 'data' => $data];
|
||||
}
|
||||
|
||||
$containerId = $data['id'];
|
||||
return ['success' => true, 'id' => $data['id'], 'data' => $data];
|
||||
}
|
||||
|
||||
// Step 2: Publish the container
|
||||
$publishUrl = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media_publish';
|
||||
$publishData = [
|
||||
'creation_id' => $containerId,
|
||||
'access_token' => $token,
|
||||
];
|
||||
private function publishContainer(string $accountId, string $token, string $containerId): array
|
||||
{
|
||||
$url = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media_publish';
|
||||
|
||||
$ch = curl_init($publishUrl);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($publishData),
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
'creation_id' => $containerId,
|
||||
'access_token' => $token,
|
||||
]),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
@@ -112,14 +214,11 @@ class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSui
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
||||
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
@@ -132,6 +231,11 @@ class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSui
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
}
|
||||
|
||||
private function isVideoUrl(string $url): bool
|
||||
{
|
||||
return (bool) preg_match('/\.(mp4|mov|avi|wmv|webm)(\?|$)/i', $url);
|
||||
}
|
||||
|
||||
public function validateCredentials(array $credentials): array
|
||||
{
|
||||
$token = $this->resolveCredential($credentials, 'access_token');
|
||||
@@ -150,13 +254,9 @@ class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSui
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
|
||||
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
@@ -183,6 +283,6 @@ class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSui
|
||||
|
||||
public function getSupportedMediaTypes(): array
|
||||
{
|
||||
return ['image', 'video'];
|
||||
return ['image', 'video', 'carousel', 'reels', 'stories'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - LinkedIn</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Mailchimp</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Mastodon</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Matrix / Element</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Medium</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - MokoSuiteCalendar Events</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - MokoSuiteGallery</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
PLG_MOKOSUITECROSS_NOSTR="MokoSuiteCross - Nostr"
|
||||
PLG_MOKOSUITECROSS_NOSTR_DESCRIPTION="Cross-post Joomla articles to Nostr."
|
||||
PLG_MOKOSUITECROSS_NOSTR_DESCRIPTION="Cross-post Joomla articles to Nostr relays via NIP-01 WebSocket protocol. Requires PHP ext-gmp for secp256k1 Schnorr signing."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Nostr</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -20,12 +20,18 @@ use Joomla\Event\SubscriberInterface;
|
||||
/**
|
||||
* Nostr service plugin for MokoSuiteCross.
|
||||
*
|
||||
* Nostr uses NIP-01 WebSocket relays for event publishing.
|
||||
* This is a stub — full WebSocket implementation is deferred.
|
||||
* Events are signed with the private key and sent to configured relays.
|
||||
* Publishes kind-1 text note events to NIP-01 WebSocket relays.
|
||||
* Uses BIP-340 Schnorr signatures over secp256k1 (requires ext-gmp).
|
||||
*
|
||||
* Credentials: private_key (64-char hex nsec), relays (comma-separated wss:// URLs)
|
||||
*/
|
||||
class NostrService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
|
||||
{
|
||||
private const SECP256K1_P = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F';
|
||||
private const SECP256K1_N = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141';
|
||||
private const SECP256K1_GX = '79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798';
|
||||
private const SECP256K1_GY = '483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8';
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
|
||||
@@ -43,6 +49,10 @@ class NostrService extends CMSPlugin implements SubscriberInterface, MokoSuiteCr
|
||||
|
||||
public function publish(string $message, array $media, array $credentials, array $params): array
|
||||
{
|
||||
if (!extension_loaded('gmp')) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'PHP ext-gmp is required for Nostr signing.']];
|
||||
}
|
||||
|
||||
$privateKey = $credentials['private_key'] ?? '';
|
||||
$relays = $credentials['relays'] ?? '';
|
||||
|
||||
@@ -50,48 +60,393 @@ class NostrService extends CMSPlugin implements SubscriberInterface, MokoSuiteCr
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing private key or relay URLs.']];
|
||||
}
|
||||
|
||||
// Nostr requires WebSocket connections to relays (wss://).
|
||||
// Full NIP-01 event signing and relay publishing is not yet implemented.
|
||||
$privateKey = strtolower(trim($privateKey));
|
||||
|
||||
if (!preg_match('/^[0-9a-f]{64}$/', $privateKey)) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Private key must be 64 hex characters.']];
|
||||
}
|
||||
|
||||
$pubkey = $this->getPublicKey($privateKey);
|
||||
|
||||
$event = $this->createEvent($pubkey, $message);
|
||||
$event['sig'] = $this->schnorrSign($event['id'], $privateKey);
|
||||
|
||||
$relayList = array_filter(array_map('trim', explode(',', $relays)));
|
||||
$lastError = '';
|
||||
$published = false;
|
||||
|
||||
foreach ($relayList as $relayUrl) {
|
||||
$result = $this->publishToRelay($relayUrl, $event);
|
||||
|
||||
if ($result['success']) {
|
||||
$published = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$lastError = $result['error'];
|
||||
}
|
||||
|
||||
if (!$published) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'All relays failed. Last: ' . $lastError]];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'platform_post_id' => '',
|
||||
'response' => ['error' => 'Nostr WebSocket relay publishing is not yet implemented. This service will be available in a future release.'],
|
||||
'success' => true,
|
||||
'platform_post_id' => $event['id'],
|
||||
'response' => ['event_id' => $event['id'], 'relay' => $relayUrl ?? ''],
|
||||
];
|
||||
}
|
||||
|
||||
public function validateCredentials(array $credentials): array
|
||||
{
|
||||
$privateKey = $credentials['private_key'] ?? '';
|
||||
if (!extension_loaded('gmp')) {
|
||||
return ['valid' => false, 'message' => 'PHP ext-gmp is required for Nostr.', 'account_name' => ''];
|
||||
}
|
||||
|
||||
$privateKey = strtolower(trim($credentials['private_key'] ?? ''));
|
||||
$relays = $credentials['relays'] ?? '';
|
||||
|
||||
if (empty($privateKey)) {
|
||||
return ['valid' => false, 'message' => 'Private key is required.', 'account_name' => ''];
|
||||
}
|
||||
|
||||
if (!preg_match('/^[0-9a-f]{64}$/', $privateKey)) {
|
||||
return ['valid' => false, 'message' => 'Private key must be 64 hex characters.', 'account_name' => ''];
|
||||
}
|
||||
|
||||
if (empty($relays)) {
|
||||
return ['valid' => false, 'message' => 'At least one relay URL is required.', 'account_name' => ''];
|
||||
}
|
||||
|
||||
// Validate that relay URLs look like WebSocket URLs
|
||||
$relayList = array_filter(array_map('trim', explode(',', $relays)));
|
||||
$valid = true;
|
||||
|
||||
foreach ($relayList as $relay) {
|
||||
if (!str_starts_with($relay, 'wss://') && !str_starts_with($relay, 'ws://')) {
|
||||
$valid = false;
|
||||
break;
|
||||
return ['valid' => false, 'message' => 'Relay URLs must start with wss:// or ws://.', 'account_name' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$valid) {
|
||||
return ['valid' => false, 'message' => 'Relay URLs must start with wss:// or ws://.', 'account_name' => ''];
|
||||
}
|
||||
$pubkey = $this->getPublicKey($privateKey);
|
||||
$npub = substr($pubkey, 0, 16) . '...';
|
||||
|
||||
return ['valid' => true, 'message' => 'Credentials configured (' . count($relayList) . ' relay(s))', 'account_name' => 'Nostr'];
|
||||
return ['valid' => true, 'message' => count($relayList) . ' relay(s) configured', 'account_name' => 'npub:' . $npub];
|
||||
}
|
||||
|
||||
public function getSupportedMediaTypes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// -- NIP-01 event creation --
|
||||
|
||||
private function createEvent(string $pubkey, string $content, int $kind = 1, array $tags = []): array
|
||||
{
|
||||
$createdAt = time();
|
||||
$serialized = json_encode([0, $pubkey, $createdAt, $kind, $tags, $content], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$id = hash('sha256', $serialized);
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'pubkey' => $pubkey,
|
||||
'created_at' => $createdAt,
|
||||
'kind' => $kind,
|
||||
'tags' => $tags,
|
||||
'content' => $content,
|
||||
'sig' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// -- WebSocket relay publishing --
|
||||
|
||||
private function publishToRelay(string $relayUrl, array $event): array
|
||||
{
|
||||
$parsed = parse_url($relayUrl);
|
||||
|
||||
if (!$parsed || !isset($parsed['host'])) {
|
||||
return ['success' => false, 'error' => 'Invalid relay URL'];
|
||||
}
|
||||
|
||||
$scheme = $parsed['scheme'] ?? 'wss';
|
||||
$host = $parsed['host'];
|
||||
$port = $parsed['port'] ?? ($scheme === 'wss' ? 443 : 80);
|
||||
$path = $parsed['path'] ?? '/';
|
||||
$useTls = ($scheme === 'wss');
|
||||
|
||||
$address = ($useTls ? 'tls://' : 'tcp://') . $host . ':' . $port;
|
||||
$context = stream_context_create(['ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]);
|
||||
|
||||
$socket = @stream_socket_client($address, $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $context);
|
||||
|
||||
if (!$socket) {
|
||||
return ['success' => false, 'error' => "Connection failed: {$errstr} ({$errno})"];
|
||||
}
|
||||
|
||||
stream_set_timeout($socket, 10);
|
||||
|
||||
// WebSocket upgrade handshake
|
||||
$wsKey = base64_encode(random_bytes(16));
|
||||
$handshake = "GET {$path} HTTP/1.1\r\n"
|
||||
. "Host: {$host}\r\n"
|
||||
. "Upgrade: websocket\r\n"
|
||||
. "Connection: Upgrade\r\n"
|
||||
. "Sec-WebSocket-Key: {$wsKey}\r\n"
|
||||
. "Sec-WebSocket-Version: 13\r\n"
|
||||
. "\r\n";
|
||||
|
||||
fwrite($socket, $handshake);
|
||||
|
||||
$response = '';
|
||||
|
||||
while (($line = fgets($socket)) !== false) {
|
||||
$response .= $line;
|
||||
|
||||
if (trim($line) === '') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($response, '101') === false) {
|
||||
fclose($socket);
|
||||
|
||||
return ['success' => false, 'error' => 'WebSocket upgrade failed'];
|
||||
}
|
||||
|
||||
// Send EVENT message
|
||||
$payload = json_encode(['EVENT', $event], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$this->wsWrite($socket, $payload);
|
||||
|
||||
// Read OK response (with timeout)
|
||||
$reply = $this->wsRead($socket);
|
||||
fclose($socket);
|
||||
|
||||
if ($reply === null) {
|
||||
return ['success' => false, 'error' => 'No response from relay'];
|
||||
}
|
||||
|
||||
$decoded = json_decode($reply, true);
|
||||
|
||||
if (!is_array($decoded) || ($decoded[0] ?? '') !== 'OK') {
|
||||
$msg = is_array($decoded) ? ($decoded[3] ?? $decoded[2] ?? 'Unknown error') : 'Invalid response';
|
||||
|
||||
return ['success' => false, 'error' => (string) $msg];
|
||||
}
|
||||
|
||||
// ["OK", event_id, true/false, message]
|
||||
$accepted = $decoded[2] ?? false;
|
||||
|
||||
if (!$accepted) {
|
||||
return ['success' => false, 'error' => $decoded[3] ?? 'Relay rejected event'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'error' => ''];
|
||||
}
|
||||
|
||||
private function wsWrite($socket, string $data): void
|
||||
{
|
||||
$len = strlen($data);
|
||||
$frame = chr(0x81); // text frame, FIN bit set
|
||||
$mask = random_bytes(4);
|
||||
|
||||
if ($len < 126) {
|
||||
$frame .= chr($len | 0x80); // mask bit set
|
||||
} elseif ($len < 65536) {
|
||||
$frame .= chr(126 | 0x80) . pack('n', $len);
|
||||
} else {
|
||||
$frame .= chr(127 | 0x80) . pack('J', $len);
|
||||
}
|
||||
|
||||
$frame .= $mask;
|
||||
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$frame .= $data[$i] ^ $mask[$i % 4];
|
||||
}
|
||||
|
||||
fwrite($socket, $frame);
|
||||
}
|
||||
|
||||
private function wsRead($socket): ?string
|
||||
{
|
||||
$header = fread($socket, 2);
|
||||
|
||||
if ($header === false || strlen($header) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$len = ord($header[1]) & 0x7F;
|
||||
|
||||
if ($len === 126) {
|
||||
$ext = fread($socket, 2);
|
||||
$len = unpack('n', $ext)[1];
|
||||
} elseif ($len === 127) {
|
||||
$ext = fread($socket, 8);
|
||||
$len = unpack('J', $ext)[1];
|
||||
}
|
||||
|
||||
$masked = (ord($header[1]) & 0x80) !== 0;
|
||||
$mask = $masked ? fread($socket, 4) : '';
|
||||
$data = '';
|
||||
|
||||
while (strlen($data) < $len) {
|
||||
$chunk = fread($socket, $len - strlen($data));
|
||||
|
||||
if ($chunk === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$data .= $chunk;
|
||||
}
|
||||
|
||||
if ($masked) {
|
||||
for ($i = 0; $i < strlen($data); $i++) {
|
||||
$data[$i] = $data[$i] ^ $mask[$i % 4];
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
// -- BIP-340 Schnorr signature over secp256k1 --
|
||||
|
||||
private function getPublicKey(string $privateKeyHex): string
|
||||
{
|
||||
$d = gmp_init($privateKeyHex, 16);
|
||||
$G = [gmp_init(self::SECP256K1_GX, 16), gmp_init(self::SECP256K1_GY, 16)];
|
||||
|
||||
$point = $this->ecMultiply($G, $d);
|
||||
|
||||
return str_pad(gmp_strval($point[0], 16), 64, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function schnorrSign(string $messageHex, string $privateKeyHex): string
|
||||
{
|
||||
$p = gmp_init(self::SECP256K1_P, 16);
|
||||
$n = gmp_init(self::SECP256K1_N, 16);
|
||||
$G = [gmp_init(self::SECP256K1_GX, 16), gmp_init(self::SECP256K1_GY, 16)];
|
||||
|
||||
$d = gmp_init($privateKeyHex, 16);
|
||||
$P = $this->ecMultiply($G, $d);
|
||||
$px = str_pad(gmp_strval($P[0], 16), 64, '0', STR_PAD_LEFT);
|
||||
|
||||
// BIP-340: if P.y is odd, negate d
|
||||
if (gmp_testbit($P[1], 0)) {
|
||||
$d = gmp_sub($n, $d);
|
||||
}
|
||||
|
||||
$dBytes = hex2bin(str_pad(gmp_strval($d, 16), 64, '0', STR_PAD_LEFT));
|
||||
$auxRand = random_bytes(32);
|
||||
$t = $dBytes ^ $this->taggedHash('BIP0340/aux', $auxRand);
|
||||
$pxBytes = hex2bin($px);
|
||||
$msgBytes = hex2bin($messageHex);
|
||||
|
||||
$rand = $this->taggedHash('BIP0340/nonce', $t . $pxBytes . $msgBytes);
|
||||
$k0 = gmp_mod(gmp_init(bin2hex($rand), 16), $n);
|
||||
|
||||
if (gmp_cmp($k0, 0) === 0) {
|
||||
return str_repeat('00', 64);
|
||||
}
|
||||
|
||||
$R = $this->ecMultiply($G, $k0);
|
||||
$k = gmp_testbit($R[1], 0) ? gmp_sub($n, $k0) : $k0;
|
||||
|
||||
$rx = str_pad(gmp_strval($R[0], 16), 64, '0', STR_PAD_LEFT);
|
||||
$rxBytes = hex2bin($rx);
|
||||
|
||||
$eHash = $this->taggedHash('BIP0340/challenge', $rxBytes . $pxBytes . $msgBytes);
|
||||
$e = gmp_mod(gmp_init(bin2hex($eHash), 16), $n);
|
||||
|
||||
$s = gmp_mod(gmp_add($k, gmp_mul($e, $d)), $n);
|
||||
|
||||
return $rx . str_pad(gmp_strval($s, 16), 64, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function taggedHash(string $tag, string $data): string
|
||||
{
|
||||
$tagHash = hash('sha256', $tag, true);
|
||||
|
||||
return hash('sha256', $tagHash . $tagHash . $data, true);
|
||||
}
|
||||
|
||||
// -- secp256k1 elliptic curve arithmetic --
|
||||
|
||||
private function ecMultiply(array $point, \GMP $scalar): array
|
||||
{
|
||||
$result = null;
|
||||
$addend = $point;
|
||||
$n = gmp_init(self::SECP256K1_N, 16);
|
||||
|
||||
$scalar = gmp_mod($scalar, $n);
|
||||
|
||||
while (gmp_cmp($scalar, 0) > 0) {
|
||||
if (gmp_testbit($scalar, 0)) {
|
||||
$result = $result === null ? $addend : $this->ecAdd($result, $addend);
|
||||
}
|
||||
|
||||
$addend = $this->ecDouble($addend);
|
||||
$scalar = gmp_div_q($scalar, 2);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function ecAdd(array $p1, array $p2): array
|
||||
{
|
||||
$prime = gmp_init(self::SECP256K1_P, 16);
|
||||
|
||||
if (gmp_cmp($p1[0], $p2[0]) === 0 && gmp_cmp($p1[1], $p2[1]) === 0) {
|
||||
return $this->ecDouble($p1);
|
||||
}
|
||||
|
||||
$dx = gmp_mod(gmp_sub($p2[0], $p1[0]), $prime);
|
||||
|
||||
if (gmp_cmp($dx, 0) < 0) {
|
||||
$dx = gmp_add($dx, $prime);
|
||||
}
|
||||
|
||||
$dy = gmp_mod(gmp_sub($p2[1], $p1[1]), $prime);
|
||||
|
||||
if (gmp_cmp($dy, 0) < 0) {
|
||||
$dy = gmp_add($dy, $prime);
|
||||
}
|
||||
|
||||
$slope = gmp_mod(gmp_mul($dy, gmp_invert($dx, $prime)), $prime);
|
||||
$x3 = gmp_mod(gmp_sub(gmp_sub(gmp_mul($slope, $slope), $p1[0]), $p2[0]), $prime);
|
||||
$y3 = gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($p1[0], $x3)), $p1[1]), $prime);
|
||||
|
||||
if (gmp_cmp($x3, 0) < 0) {
|
||||
$x3 = gmp_add($x3, $prime);
|
||||
}
|
||||
|
||||
if (gmp_cmp($y3, 0) < 0) {
|
||||
$y3 = gmp_add($y3, $prime);
|
||||
}
|
||||
|
||||
return [$x3, $y3];
|
||||
}
|
||||
|
||||
private function ecDouble(array $point): array
|
||||
{
|
||||
$prime = gmp_init(self::SECP256K1_P, 16);
|
||||
|
||||
// secp256k1 has a=0, so slope = 3*x^2 / (2*y)
|
||||
$num = gmp_mod(gmp_mul(3, gmp_mul($point[0], $point[0])), $prime);
|
||||
$denom = gmp_mod(gmp_mul(2, $point[1]), $prime);
|
||||
|
||||
if (gmp_cmp($denom, 0) < 0) {
|
||||
$denom = gmp_add($denom, $prime);
|
||||
}
|
||||
|
||||
$slope = gmp_mod(gmp_mul($num, gmp_invert($denom, $prime)), $prime);
|
||||
$x3 = gmp_mod(gmp_sub(gmp_mul($slope, $slope), gmp_mul(2, $point[0])), $prime);
|
||||
$y3 = gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($point[0], $x3)), $point[1]), $prime);
|
||||
|
||||
if (gmp_cmp($x3, 0) < 0) {
|
||||
$x3 = gmp_add($x3, $prime);
|
||||
}
|
||||
|
||||
if (gmp_cmp($y3, 0) < 0) {
|
||||
$y3 = gmp_add($y3, $prime);
|
||||
}
|
||||
|
||||
return [$x3, $y3];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Ntfy Push Notifications</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Pinterest</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Reddit</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - RSS Feed</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - SendGrid</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Slack</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Microsoft Teams</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Telegram</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -17,15 +17,13 @@ use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
/**
|
||||
* Threads (Meta) service plugin for MokoSuiteCross.
|
||||
*
|
||||
* Uses the Threads Publishing API — a 2-step flow:
|
||||
* 1. Create a media container via POST /{user_id}/threads
|
||||
* 2. Publish the container via POST /{user_id}/threads_publish
|
||||
*/
|
||||
class ThreadsService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
|
||||
{
|
||||
private const API_BASE = 'https://graph.threads.net/v1.0/';
|
||||
private const MAX_CAROUSEL_ITEMS = 20;
|
||||
private const MAX_POLL_OPTIONS = 4;
|
||||
private const MIN_POLL_OPTIONS = 2;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
|
||||
@@ -50,62 +48,104 @@ class ThreadsService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or user ID.']];
|
||||
}
|
||||
|
||||
// Step 1: Create media container
|
||||
$containerUrl = 'https://graph.threads.net/v1.0/' . urlencode($userId) . '/threads';
|
||||
$text = mb_substr($message, 0, 500);
|
||||
$media = array_filter($media);
|
||||
|
||||
if (\count($media) > 1) {
|
||||
return $this->publishCarousel($text, $media, $userId, $token, $params);
|
||||
}
|
||||
|
||||
return $this->publishSinglePost($text, $media, $userId, $token, $params);
|
||||
}
|
||||
|
||||
private function publishSinglePost(string $text, array $media, string $userId, string $token, array $params): array
|
||||
{
|
||||
$containerUrl = self::API_BASE . urlencode($userId) . '/threads';
|
||||
$containerData = [
|
||||
'text' => mb_substr($message, 0, 500),
|
||||
'text' => $text,
|
||||
'access_token' => $token,
|
||||
];
|
||||
|
||||
// Attach image if provided
|
||||
if (!empty($media[0])) {
|
||||
$containerData['media_type'] = 'IMAGE';
|
||||
$containerData['image_url'] = $media[0];
|
||||
$mediaType = $this->detectMediaType($media[0]);
|
||||
$containerData['media_type'] = $mediaType;
|
||||
$containerData[$mediaType === 'VIDEO' ? 'video_url' : 'image_url'] = $media[0];
|
||||
} else {
|
||||
$containerData['media_type'] = 'TEXT';
|
||||
}
|
||||
|
||||
$ch = curl_init($containerUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($containerData),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$this->applyPollOptions($containerData, $params);
|
||||
$this->applySpoilerFlag($containerData, $params);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$result = $this->apiPost($containerUrl, $containerData);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
||||
|
||||
}
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true) ?: [];
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300 || empty($data['id'])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$containerId = $data['id'];
|
||||
return $this->publishContainer($userId, $token, $result['response']['id']);
|
||||
}
|
||||
|
||||
// Step 2: Publish the container
|
||||
$publishUrl = 'https://graph.threads.net/v1.0/' . urlencode($userId) . '/threads_publish';
|
||||
private function publishCarousel(string $text, array $media, string $userId, string $token, array $params): array
|
||||
{
|
||||
$items = \array_slice($media, 0, self::MAX_CAROUSEL_ITEMS);
|
||||
$childIds = [];
|
||||
|
||||
foreach ($items as $url) {
|
||||
$mediaType = $this->detectMediaType($url);
|
||||
$childData = [
|
||||
'is_carousel_item' => 'true',
|
||||
'media_type' => $mediaType,
|
||||
'access_token' => $token,
|
||||
];
|
||||
$childData[$mediaType === 'VIDEO' ? 'video_url' : 'image_url'] = $url;
|
||||
|
||||
$childUrl = self::API_BASE . urlencode($userId) . '/threads';
|
||||
$result = $this->apiPost($childUrl, $childData);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$childIds[] = $result['response']['id'];
|
||||
}
|
||||
|
||||
$carouselData = [
|
||||
'media_type' => 'CAROUSEL',
|
||||
'children' => implode(',', $childIds),
|
||||
'text' => $text,
|
||||
'access_token' => $token,
|
||||
];
|
||||
|
||||
$this->applySpoilerFlag($carouselData, $params);
|
||||
|
||||
$carouselUrl = self::API_BASE . urlencode($userId) . '/threads';
|
||||
$result = $this->apiPost($carouselUrl, $carouselData);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->publishContainer($userId, $token, $result['response']['id']);
|
||||
}
|
||||
|
||||
private function publishContainer(string $userId, string $token, string $containerId): array
|
||||
{
|
||||
$publishUrl = self::API_BASE . urlencode($userId) . '/threads_publish';
|
||||
$publishData = [
|
||||
'creation_id' => $containerId,
|
||||
'access_token' => $token,
|
||||
];
|
||||
|
||||
$ch = curl_init($publishUrl);
|
||||
return $this->apiPost($publishUrl, $publishData);
|
||||
}
|
||||
|
||||
private function apiPost(string $url, array $data): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($publishData),
|
||||
CURLOPT_POSTFIELDS => http_build_query($data),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
@@ -113,24 +153,60 @@ class ThreadsService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
||||
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true) ?: [];
|
||||
$responseData = json_decode($response, true) ?: [];
|
||||
|
||||
if ($httpCode >= 200 && $httpCode < 300 && !empty($data['id'])) {
|
||||
return ['success' => true, 'platform_post_id' => (string) $data['id'], 'response' => $data];
|
||||
if ($httpCode >= 200 && $httpCode < 300 && !empty($responseData['id'])) {
|
||||
return ['success' => true, 'platform_post_id' => (string) $responseData['id'], 'response' => $responseData];
|
||||
}
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $responseData];
|
||||
}
|
||||
|
||||
private function applyPollOptions(array &$data, array $params): void
|
||||
{
|
||||
$options = $params['poll_options'] ?? [];
|
||||
|
||||
if (empty($options) || !\is_array($options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$options = \array_slice($options, 0, self::MAX_POLL_OPTIONS);
|
||||
|
||||
if (\count($options) < self::MIN_POLL_OPTIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['poll'] = json_encode(['options' => array_values($options)]);
|
||||
}
|
||||
|
||||
private function applySpoilerFlag(array &$data, array $params): void
|
||||
{
|
||||
if (!empty($params['spoiler'])) {
|
||||
$data['spoiler'] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
private function detectMediaType(string $url): string
|
||||
{
|
||||
$path = strtolower(parse_url($url, PHP_URL_PATH) ?? '');
|
||||
$videoExtensions = ['.mp4', '.mov', '.avi', '.wmv', '.webm'];
|
||||
|
||||
foreach ($videoExtensions as $ext) {
|
||||
if (str_ends_with($path, $ext)) {
|
||||
return 'VIDEO';
|
||||
}
|
||||
}
|
||||
|
||||
return 'IMAGE';
|
||||
}
|
||||
|
||||
public function validateCredentials(array $credentials): array
|
||||
@@ -142,7 +218,7 @@ class ThreadsService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
||||
return ['valid' => false, 'message' => 'Access token and user ID are required.', 'account_name' => ''];
|
||||
}
|
||||
|
||||
$ch = curl_init('https://graph.threads.net/v1.0/' . urlencode($userId) . '?fields=username&access_token=' . urlencode($token));
|
||||
$ch = curl_init(self::API_BASE . urlencode($userId) . '?fields=username&access_token=' . urlencode($token));
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Threads (Meta)</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - TikTok</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Tumblr</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - X / Twitter</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Generic Webhook</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - WhatsApp Business</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - WordPress</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Youtube</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteCross</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteCross Events</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteCross Gallery</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteCross Queue Processor</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteCross</name>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
+2
-2
@@ -35,8 +35,9 @@ class MokoSuiteCrossWebServices extends CMSPlugin implements SubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function onBeforeApiRoute(&$router): void
|
||||
public function onBeforeApiRoute($event): void
|
||||
{
|
||||
$router = $event instanceof \Joomla\CMS\Event\AbstractEvent ? $event->getRouter() : $event;
|
||||
$defaults = ['component' => 'com_mokosuitecross'];
|
||||
|
||||
$router->createCRUDRoutes('v1/mokosuitecross/posts', 'posts', $defaults);
|
||||
@@ -44,7 +45,6 @@ class MokoSuiteCrossWebServices extends CMSPlugin implements SubscriberInterface
|
||||
$router->createCRUDRoutes('v1/mokosuitecross/templates', 'templates', $defaults);
|
||||
$router->createCRUDRoutes('v1/mokosuitecross/logs', 'logs', $defaults);
|
||||
|
||||
// Action endpoint: dispatch cross-posts for an article (POST only)
|
||||
$router->addRoute(
|
||||
new \Joomla\Router\Route(['POST'], 'v1/mokosuitecross/dispatch', 'dispatch.dispatch', [], $defaults)
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>MokoSuiteCross</name>
|
||||
<packagename>mokosuitecross</packagename>
|
||||
<version>01.08.00</version>
|
||||
<version>01.08.18</version>
|
||||
<creationDate>2026-05-28</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user