Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2080375343 | |||
| 8e748c2072 | |||
| 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,41 +0,0 @@
|
||||
# EditorConfig helps maintain consistent coding styles across different editors and IDEs
|
||||
# https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
# Default settings — Tabs preferred, width = 2 spaces
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
# PowerShell scripts — tabs, 2-space visual width
|
||||
[*.ps1]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
end_of_line = crlf
|
||||
|
||||
# Markdown files — keep trailing whitespace for line breaks
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# JSON / YAML files — tabs, 2-space visual width
|
||||
[*.{json,yml,yaml}]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
# Makefiles — always tabs, default width
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
# Windows batch scripts — keep CRLF endings
|
||||
[*.{bat,cmd}]
|
||||
end_of_line = crlf
|
||||
|
||||
# Shell scripts — ensure LF endings
|
||||
[*.sh]
|
||||
end_of_line = lf
|
||||
@@ -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
@@ -20,7 +20,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
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 }}
|
||||
|
||||
@@ -55,14 +55,14 @@ jobs:
|
||||
|
||||
- name: Validate metadata against Joomla manifest
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
||||
--path . \
|
||||
--token "${MOKOGITEA_TOKEN}" \
|
||||
--token "${GITEA_TOKEN}" \
|
||||
--org "${GITEA_ORG}" \
|
||||
--repo "${GITEA_REPO}" \
|
||||
--api-base "${MOKOGITEA_URL}/api/v1" \
|
||||
--api-base "${GITEA_URL}/api/v1" \
|
||||
--ci
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
|
||||
@@ -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: |
|
||||
|
||||
+40
-2
@@ -1,9 +1,25 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
## [01.08.00] --- 2026-06-23
|
||||
### Added
|
||||
- **X/Twitter threads**: Auto-split messages exceeding 280 chars into reply chains at sentence boundaries
|
||||
- **X/Twitter cost-optimized posting**: Optional mode to post text-only tweet first ($0.015) with URL as separate reply ($0.20)
|
||||
- **X/Twitter cost warning**: Language string documenting X API pricing for text vs URL posts
|
||||
- **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
|
||||
|
||||
## [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 +32,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 +69,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,46 +0,0 @@
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
|
||||
VERSION: 01.01.00
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Community expectations and enforcement guidelines
|
||||
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
||||
-->
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
|
||||
|
||||
## Our Standards
|
||||
- Be empathetic and kind
|
||||
- Be respectful of differing opinions
|
||||
- Accept constructive feedback
|
||||
- Own mistakes and learn from them
|
||||
|
||||
Unacceptable behavior includes sexualized language/imagery, trolling, harassment, doxing, and other inappropriate conduct.
|
||||
|
||||
## Enforcement
|
||||
Report incidents to **hello@mokoconsulting.tech** or through GitHub Discussions if you prefer a community-visible approach. Private complaints will be reviewed promptly and fairly.
|
||||
|
||||
## Enforcement Guidelines
|
||||
1. **Correction** — Private warning
|
||||
2. **Warning** — Formal warning and limited interaction
|
||||
3. **Temporary Ban** — Time-boxed exclusion
|
||||
4. **Permanent Ban** — Removal from the community
|
||||
|
||||
## Attribution
|
||||
Adapted from the Contributor Covenant v2.1.
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of
|
||||
the GNU General Public License as published by the Free Software Foundation; either version 3
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||
|
||||
FILE INFORMATION
|
||||
DEFGROUP: mokoconsulting-tech.Template-Joomla
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla
|
||||
VERSION: 01.01.00
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
|
||||
-->
|
||||
|
||||
[](https://github.com/mokoconsulting-tech/MokoStandards)
|
||||
|
||||
# Project Governance
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the governance model for the `Template-Joomla` repository within the
|
||||
`mokoconsulting-tech` organization. It is automatically maintained by
|
||||
[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04.
|
||||
|
||||
Full governance policy is defined in the MokoStandards source repository:
|
||||
[docs/policy/GOVERNANCE.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md)
|
||||
|
||||
---
|
||||
|
||||
## Roles and Responsibilities
|
||||
|
||||
### Maintainer
|
||||
|
||||
**GitHub**: @mokoconsulting-tech
|
||||
|
||||
**Authority**: Final decision-making authority on all matters for this repository.
|
||||
|
||||
**Responsibilities**:
|
||||
- Review and merge pull requests
|
||||
- Maintain code quality and standards compliance
|
||||
- Manage releases and versioning
|
||||
- Respond to issues and security reports
|
||||
|
||||
### Contributors
|
||||
|
||||
**Authority**: Submit changes via pull requests.
|
||||
|
||||
**Requirements**:
|
||||
- Read and accept `CODE_OF_CONDUCT.md`
|
||||
- Follow `CONTRIBUTING.md` guidelines
|
||||
|
||||
---
|
||||
|
||||
## Decision-Making
|
||||
|
||||
All changes must be submitted as pull requests. The maintainer (@mokoconsulting-tech)
|
||||
reviews and approves all changes before they are merged.
|
||||
|
||||
### Sole Operator Policy
|
||||
|
||||
This organization operates under a **sole operator** model. The maintainer (@mokoconsulting-tech)
|
||||
is the sole employee and owner and may self-approve pull requests when no second reviewer is
|
||||
available. The following requirements remain mandatory regardless:
|
||||
|
||||
1. **Pull Requests Required** — all changes to protected branches go through a PR.
|
||||
2. **Automated Checks** — all CI checks must pass before merging.
|
||||
3. **Audit Trail** — issues, pull requests, and commit history are preserved.
|
||||
4. **Documentation** — changes are documented in `CHANGELOG.md`.
|
||||
|
||||
See the full policy:
|
||||
[Sole Operator Policy](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md#sole-operator-policy)
|
||||
|
||||
---
|
||||
|
||||
## Change Management
|
||||
|
||||
| Change Type | Approval | Process |
|
||||
|-------------|----------|---------|
|
||||
| Routine (docs, bug fixes) | Maintainer | PR → CI pass → merge |
|
||||
| Significant (new features) | Maintainer | PR with description → CI pass → merge |
|
||||
| Major (breaking, architecture) | Maintainer | Issue discussion → PR → CI pass → merge |
|
||||
| Emergency (security) | Maintainer | Labelled `EMERGENCY` → immediate merge → post-mortem |
|
||||
|
||||
---
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/Template-Joomla/issues)
|
||||
- **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md)
|
||||
- **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
||||
- **Contact**: dev@mokoconsulting.tech
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| ------------- | ----------------------------------------------- |
|
||||
| Document Type | Policy |
|
||||
| Domain | Governance |
|
||||
| Applies To | mokoconsulting-tech/Template-Joomla |
|
||||
| Jurisdiction | Tennessee, USA |
|
||||
| Maintainer | @mokoconsulting-tech |
|
||||
| Standards | MokoStandards v04.00.04 |
|
||||
| Repo | https://github.com/mokoconsulting-tech/Template-Joomla |
|
||||
| Path | /GOVERNANCE.md |
|
||||
| Status | Active — auto-maintained by MokoStandards |
|
||||
@@ -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.
|
||||
|
||||
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 01.01.00
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
# Security Policy
|
||||
|
||||
## Purpose and Scope
|
||||
|
||||
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are provided for the following versions:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 01.x.x | :white_check_mark: |
|
||||
| < 01.0 | :x: |
|
||||
|
||||
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
### Where to Report
|
||||
|
||||
**DO NOT** create public GitHub issues for security vulnerabilities.
|
||||
|
||||
Report security vulnerabilities privately to:
|
||||
|
||||
**Email**: `security@mokoconsulting.tech`
|
||||
|
||||
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
|
||||
|
||||
### What to Include
|
||||
|
||||
A complete vulnerability report should include:
|
||||
|
||||
1. **Description**: Clear explanation of the vulnerability
|
||||
2. **Impact**: Potential security impact and severity assessment
|
||||
3. **Affected Versions**: Which versions are vulnerable
|
||||
4. **Reproduction Steps**: Detailed steps to reproduce the issue
|
||||
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
|
||||
6. **Suggested Fix**: Proposed remediation (if known)
|
||||
7. **Disclosure Timeline**: Your expectations for public disclosure
|
||||
|
||||
### Response Timeline
|
||||
|
||||
* **Initial Response**: Within 3 business days
|
||||
* **Assessment Complete**: Within 7 business days
|
||||
* **Fix Timeline**: Depends on severity (see below)
|
||||
* **Disclosure**: Coordinated with reporter
|
||||
|
||||
## Severity Classification
|
||||
|
||||
Vulnerabilities are classified using the following severity levels:
|
||||
|
||||
### Critical
|
||||
* Remote code execution
|
||||
* Authentication bypass
|
||||
* Data breach or exposure of sensitive information
|
||||
* **Fix Timeline**: 7 days
|
||||
|
||||
### High
|
||||
* Privilege escalation
|
||||
* SQL injection or command injection
|
||||
* Cross-site scripting (XSS) with significant impact
|
||||
* **Fix Timeline**: 14 days
|
||||
|
||||
### Medium
|
||||
* Information disclosure (limited scope)
|
||||
* Denial of service
|
||||
* Security misconfigurations with moderate impact
|
||||
* **Fix Timeline**: 30 days
|
||||
|
||||
### Low
|
||||
* Security best practice violations
|
||||
* Minor information leaks
|
||||
* Issues requiring user interaction or complex preconditions
|
||||
* **Fix Timeline**: 60 days or next release
|
||||
|
||||
## Remediation Process
|
||||
|
||||
1. **Acknowledgment**: Security team confirms receipt and begins investigation
|
||||
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
|
||||
3. **Development**: Security patch is developed and tested
|
||||
4. **Review**: Patch undergoes security review and validation
|
||||
5. **Release**: Fixed version is released with security advisory
|
||||
6. **Disclosure**: Public disclosure follows coordinated timeline
|
||||
|
||||
## Security Advisories
|
||||
|
||||
Security advisories are published via:
|
||||
|
||||
* GitHub Security Advisories
|
||||
* Release notes and CHANGELOG.md
|
||||
* Email notification to project users (if mailing list is established)
|
||||
|
||||
Advisories include:
|
||||
|
||||
* CVE identifier (if applicable)
|
||||
* Severity rating
|
||||
* Affected versions
|
||||
* Fixed versions
|
||||
* Mitigation steps
|
||||
* Attribution (with reporter consent)
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
For projects using this template:
|
||||
|
||||
### Required Controls
|
||||
|
||||
* Enable GitHub security features (Dependabot, code scanning)
|
||||
* Implement branch protection on `main`
|
||||
* Require code review for all changes
|
||||
* Enforce signed commits (recommended)
|
||||
* Use secrets management (never commit credentials)
|
||||
* Maintain security documentation
|
||||
* Follow secure coding standards defined in MokoStandards
|
||||
|
||||
### Joomla Plugin Security
|
||||
|
||||
* Follow Joomla security best practices
|
||||
* Validate and sanitize all user input
|
||||
* Use Joomla's database API to prevent SQL injection
|
||||
* Properly escape output to prevent XSS
|
||||
* Implement proper access control checks
|
||||
* Use Joomla's session and authentication APIs
|
||||
* Keep Joomla and dependencies up to date
|
||||
|
||||
### CI/CD Security
|
||||
|
||||
* Validate all inputs
|
||||
* Sanitize outputs
|
||||
* Use least privilege access
|
||||
* Pin dependencies with hash verification
|
||||
* Scan for vulnerabilities in dependencies
|
||||
* Audit third-party actions and tools
|
||||
|
||||
#### Automated Security Scanning
|
||||
|
||||
All repositories SHOULD implement:
|
||||
|
||||
**CodeQL Analysis**:
|
||||
* Enabled for PHP and other supported languages
|
||||
* Runs on: push to main, pull requests, weekly schedule
|
||||
* Query sets: `security-extended` and `security-and-quality`
|
||||
* Configuration: `.github/workflows/codeql-analysis.yml`
|
||||
|
||||
**Dependabot Security Updates**:
|
||||
* Weekly scans for vulnerable dependencies
|
||||
* Automated pull requests for security patches
|
||||
* Configuration: `.github/dependabot.yml`
|
||||
|
||||
**Secret Scanning**:
|
||||
* Enabled by default with push protection
|
||||
* Prevents accidental credential commits
|
||||
|
||||
### Dependency Management
|
||||
|
||||
* Keep dependencies up to date
|
||||
* Monitor security advisories for dependencies
|
||||
* Remove unused dependencies
|
||||
* Audit new dependencies before adoption
|
||||
* Document security-critical dependencies
|
||||
|
||||
## Compliance and Governance
|
||||
|
||||
This security policy is aligned with MokoStandards. Deviations require documented justification.
|
||||
|
||||
Security policies are reviewed and updated at least annually or following significant security incidents.
|
||||
|
||||
## Attribution and Recognition
|
||||
|
||||
We acknowledge and appreciate responsible disclosure. With your permission, we will:
|
||||
|
||||
* Credit you in security advisories
|
||||
* List you in CHANGELOG.md for the fix release
|
||||
* Recognize your contribution publicly (if desired)
|
||||
|
||||
## Contact and Escalation
|
||||
|
||||
* **Security Team**: security@mokoconsulting.tech
|
||||
* **Primary Contact**: hello@mokoconsulting.tech
|
||||
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are explicitly out of scope:
|
||||
|
||||
* Issues in third-party dependencies (report directly to maintainers)
|
||||
* Social engineering attacks
|
||||
* Physical security issues
|
||||
* Denial of service via resource exhaustion without amplification
|
||||
* Issues requiring physical access to systems
|
||||
* Theoretical vulnerabilities without proof of exploitability
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| Document | Security Policy |
|
||||
| Path | /SECURITY.md |
|
||||
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
|
||||
| Owner | Moko Consulting |
|
||||
| Scope | Security vulnerability handling |
|
||||
| Status | Active |
|
||||
| Effective | 2026-01-16 |
|
||||
|
||||
## Revision History
|
||||
|
||||
| Date | Change Description | Author |
|
||||
| ---------- | ------------------------------------------------- | --------------- |
|
||||
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mokoconsulting/mokojoomgallery",
|
||||
"description": "Photo gallery management for Joomla — galleries, images, thumbnails, lightbox, and frontend display",
|
||||
"name": "mokoconsulting/mokosuitecross",
|
||||
"description": "Cross-posting Joomla content to social media, email marketing, and chat platforms",
|
||||
"type": "joomla-package",
|
||||
"version": "01.00.00",
|
||||
"license": "GPL-3.0-or-later",
|
||||
@@ -17,10 +17,8 @@
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"joomla/coding-standards": "3.0.x-dev"
|
||||
"joomla/coding-standards": "^4.0"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# PHPStan configuration for Joomla extension repositories.
|
||||
# Extends the base MokoStandards config and adds Joomla framework class stubs
|
||||
# so PHPStan can resolve Factory, CMSApplication, User, Table, etc.
|
||||
# without requiring a full Joomla installation.
|
||||
|
||||
parameters:
|
||||
level: 5
|
||||
|
||||
paths:
|
||||
- src
|
||||
|
||||
excludePaths:
|
||||
- vendor
|
||||
- node_modules
|
||||
|
||||
# Joomla framework stubs — resolved via the enterprise package from vendor/
|
||||
stubFiles:
|
||||
- vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php
|
||||
|
||||
# Suppress errors that are structural in Joomla's service-container architecture
|
||||
ignoreErrors:
|
||||
# Joomla's service-based dependency injection returns mixed from getApplication()
|
||||
- '#Cannot call method .+ on Joomla\\CMS\\Application\\CMSApplication\|null#'
|
||||
# Factory::getX() patterns are safe at runtime even when nullable in stubs
|
||||
- '#Call to static method [a-zA-Z]+\(\) on an interface#'
|
||||
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
checkMissingIterableValueType: false
|
||||
checkGenericClassInNonGenericObjectType: false
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -1,2 +1,3 @@
|
||||
PLG_MOKOSUITECROSS_TWITTER="MokoSuiteCross - X / Twitter"
|
||||
PLG_MOKOSUITECROSS_TWITTER_DESCRIPTION="Cross-post Joomla articles to X / Twitter."
|
||||
PLG_MOKOSUITECROSS_TWITTER_COST_WARNING="X API Pricing: Text-only posts cost $0.015 each. Posts containing URLs cost $0.20 each. Cross-posting articles with links will incur URL post charges."
|
||||
|
||||
@@ -51,9 +51,6 @@ class TwitterService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
||||
|
||||
public function publish(string $message, array $media, array $credentials, array $params): array
|
||||
{
|
||||
$apiUrl = 'https://api.twitter.com/2/tweets';
|
||||
$postData = json_encode(['text' => mb_substr($message, 0, 280)]);
|
||||
|
||||
$consumerKey = $credentials['api_key'] ?? '';
|
||||
$consumerSecret = $credentials['api_secret'] ?? '';
|
||||
$accessToken = $credentials['access_token'] ?? '';
|
||||
@@ -67,41 +64,17 @@ class TwitterService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
||||
];
|
||||
}
|
||||
|
||||
$authHeader = $this->buildOAuth1Header('POST', $apiUrl, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: ' . $authHeader,
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$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 === 201 && !empty($data['data']['id'])) {
|
||||
return ['success' => true, 'platform_post_id' => $data['data']['id'], 'response' => $data];
|
||||
if (!empty($params['cost_optimize'])) {
|
||||
return $this->publishCostOptimized($message, $credentials, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
}
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
$chunks = $this->splitIntoThread($message);
|
||||
|
||||
if (\count($chunks) === 1) {
|
||||
return $this->postTweet($chunks[0], null, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
}
|
||||
|
||||
return $this->postThread($chunks, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
}
|
||||
|
||||
public function validateCredentials(array $credentials): array
|
||||
@@ -158,6 +131,201 @@ class TwitterService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
||||
return true;
|
||||
}
|
||||
|
||||
private function splitIntoThread(string $message, int $maxLen = 280): array
|
||||
{
|
||||
if (mb_strlen($message) <= $maxLen) {
|
||||
return [$message];
|
||||
}
|
||||
|
||||
$chunks = [];
|
||||
|
||||
while (mb_strlen($message) > $maxLen) {
|
||||
$segment = mb_substr($message, 0, $maxLen);
|
||||
|
||||
$splitPos = false;
|
||||
|
||||
foreach (['. ', '! ', '? '] as $delimiter) {
|
||||
$pos = mb_strrpos($segment, $delimiter);
|
||||
|
||||
if ($pos !== false && ($splitPos === false || $pos > $splitPos)) {
|
||||
$splitPos = $pos + mb_strlen($delimiter) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($splitPos === false || $splitPos < 1) {
|
||||
$splitPos = mb_strrpos($segment, ' ');
|
||||
}
|
||||
|
||||
if ($splitPos === false || $splitPos < 1) {
|
||||
$splitPos = $maxLen;
|
||||
}
|
||||
|
||||
$chunks[] = trim(mb_substr($message, 0, $splitPos));
|
||||
$message = trim(mb_substr($message, $splitPos));
|
||||
}
|
||||
|
||||
if ($message !== '') {
|
||||
$chunks[] = $message;
|
||||
}
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
private function postTweet(
|
||||
string $text,
|
||||
?string $replyToId,
|
||||
string $consumerKey,
|
||||
string $consumerSecret,
|
||||
string $accessToken,
|
||||
string $tokenSecret
|
||||
): array {
|
||||
$apiUrl = 'https://api.twitter.com/2/tweets';
|
||||
|
||||
$body = ['text' => $text];
|
||||
|
||||
if ($replyToId !== null) {
|
||||
$body['reply'] = ['in_reply_to_tweet_id' => $replyToId];
|
||||
}
|
||||
|
||||
$postData = json_encode($body);
|
||||
$authHeader = $this->buildOAuth1Header('POST', $apiUrl, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: ' . $authHeader,
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$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 === 201 && !empty($data['data']['id'])) {
|
||||
return ['success' => true, 'platform_post_id' => $data['data']['id'], 'response' => $data];
|
||||
}
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
}
|
||||
|
||||
private function postThread(
|
||||
array $chunks,
|
||||
string $consumerKey,
|
||||
string $consumerSecret,
|
||||
string $accessToken,
|
||||
string $tokenSecret
|
||||
): array {
|
||||
$firstResult = $this->postTweet($chunks[0], null, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
|
||||
if (!$firstResult['success']) {
|
||||
return $firstResult;
|
||||
}
|
||||
|
||||
$rootId = $firstResult['platform_post_id'];
|
||||
$previousId = $rootId;
|
||||
|
||||
for ($i = 1, $count = \count($chunks); $i < $count; $i++) {
|
||||
$result = $this->postTweet($chunks[$i], $previousId, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
|
||||
if (!$result['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'platform_post_id' => $rootId,
|
||||
'response' => [
|
||||
'error' => 'Thread failed at tweet ' . ($i + 1) . ' of ' . $count,
|
||||
'root_tweet' => $rootId,
|
||||
'failed_tweet' => $result['response'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$previousId = $result['platform_post_id'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'platform_post_id' => $rootId, 'response' => $firstResult['response']];
|
||||
}
|
||||
|
||||
private function publishCostOptimized(
|
||||
string $message,
|
||||
array $credentials,
|
||||
string $consumerKey,
|
||||
string $consumerSecret,
|
||||
string $accessToken,
|
||||
string $tokenSecret
|
||||
): array {
|
||||
$urlPattern = '/https?:\/\/\S+/';
|
||||
$urls = [];
|
||||
preg_match_all($urlPattern, $message, $urls);
|
||||
$urls = $urls[0] ?? [];
|
||||
|
||||
$textOnly = trim(preg_replace($urlPattern, '', $message));
|
||||
$textOnly = preg_replace('/\s{2,}/', ' ', $textOnly);
|
||||
|
||||
if ($textOnly === '' && $urls === []) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Empty message after URL extraction.']];
|
||||
}
|
||||
|
||||
$textChunks = $textOnly !== '' ? $this->splitIntoThread($textOnly) : [];
|
||||
|
||||
if ($textChunks === [] && $urls !== []) {
|
||||
$textChunks = [implode(' ', $urls)];
|
||||
$urls = [];
|
||||
}
|
||||
|
||||
$firstResult = $this->postTweet($textChunks[0], null, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
|
||||
if (!$firstResult['success']) {
|
||||
return $firstResult;
|
||||
}
|
||||
|
||||
$rootId = $firstResult['platform_post_id'];
|
||||
$previousId = $rootId;
|
||||
|
||||
for ($i = 1, $count = \count($textChunks); $i < $count; $i++) {
|
||||
$result = $this->postTweet($textChunks[$i], $previousId, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
|
||||
if (!$result['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'platform_post_id' => $rootId,
|
||||
'response' => ['error' => 'Cost-optimized thread failed at tweet ' . ($i + 1), 'root_tweet' => $rootId],
|
||||
];
|
||||
}
|
||||
|
||||
$previousId = $result['platform_post_id'];
|
||||
}
|
||||
|
||||
if ($urls !== []) {
|
||||
$urlText = implode(' ', $urls);
|
||||
$result = $this->postTweet($urlText, $previousId, $consumerKey, $consumerSecret, $accessToken, $tokenSecret);
|
||||
|
||||
if (!$result['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'platform_post_id' => $rootId,
|
||||
'response' => ['error' => 'Cost-optimized URL reply failed.', 'root_tweet' => $rootId],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'platform_post_id' => $rootId, 'response' => $firstResult['response']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an OAuth 1.0a Authorization header with HMAC-SHA1 signature.
|
||||
*/
|
||||
|
||||
@@ -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