Retire Twitter and GA V3 webhooks.

This commit is contained in:
Buster Neece 2023-08-27 21:19:46 -05:00
parent 6eeb864ac6
commit fd4876e5aa
No known key found for this signature in database
13 changed files with 82 additions and 455 deletions

View File

@ -11,6 +11,10 @@ release channel, you can take advantage of these new features and fixes.
## Code Quality/Technical Changes
- The Google Analytics V3 and Twitter (now X) web hooks have been retired and are no longer supported. Google Analytics
has removed support for V3 properties entirely; we recommend switching to Analytics V4. Twitter has deprecated version
1 of their API in favor of version 2, which is not readily accessible to applications like ours.
## Bug Fixes
---

View File

@ -41,7 +41,6 @@
"gettext/gettext": "^5",
"gettext/php-scanner": "^1.3",
"guzzlehttp/guzzle": "^7.0",
"guzzlehttp/oauth-subscriber": "^0.6.0",
"intervention/image": "^2.6",
"james-heinrich/getid3": "v2.0.0-beta5",
"league/csv": "^9.6",

61
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fe3a67cfc6f90897593aef83f8a54767",
"content-hash": "cc878a9297f83d6f4b49cbf1f17ddd7f",
"packages": [
{
"name": "aws/aws-crt-php",
@ -2546,65 +2546,6 @@
],
"time": "2023-05-21T14:04:53+00:00"
},
{
"name": "guzzlehttp/oauth-subscriber",
"version": "0.6.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/oauth-subscriber.git",
"reference": "8d6cab29f8397e5712d00a383eeead36108a3c1f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/oauth-subscriber/zipball/8d6cab29f8397e5712d00a383eeead36108a3c1f",
"reference": "8d6cab29f8397e5712d00a383eeead36108a3c1f",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^6.5|^7.2",
"guzzlehttp/psr7": "^1.7|^2.0",
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0|^9.3.3"
},
"suggest": {
"ext-openssl": "Required to sign using RSA-SHA1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.6-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Subscriber\\Oauth\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle OAuth 1.0 subscriber",
"homepage": "http://guzzlephp.org/",
"keywords": [
"Guzzle",
"oauth"
],
"support": {
"issues": "https://github.com/guzzle/oauth-subscriber/issues",
"source": "https://github.com/guzzle/oauth-subscriber/tree/0.6.0"
},
"time": "2021-07-13T12:01:32+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.0.1",

View File

@ -77,9 +77,7 @@ export enum WebhookType {
RadioDe = 'radiode',
Discord = 'discord',
Telegram = 'telegram',
Twitter = 'twitter',
Mastodon = 'mastodon',
GoogleAnalyticsV3 = 'google_analytics',
GoogleAnalyticsV4 = 'google_analytics_v4',
MatomoAnalytics = 'matomo_analytics'
}
@ -112,18 +110,10 @@ export function useTypeDetails() {
title: $gettext('Telegram Chat Message'),
description: $gettext('Use the Telegram Bot API to send a message to a channel.')
},
[WebhookType.Twitter]: {
title: $gettext('Twitter Post'),
description: $gettext('Automatically send a tweet.')
},
[WebhookType.Mastodon]: {
title: $gettext('Mastodon Post'),
description: $gettext('Automatically publish to a Mastodon instance.')
},
[WebhookType.GoogleAnalyticsV3]: {
title: $gettext('Google Analytics V3 Integration'),
description: $gettext('Send stream listener details to Google Analytics.')
},
[WebhookType.GoogleAnalyticsV4]: {
title: $gettext('Google Analytics V4 Integration'),
description: $gettext('Send stream listener details to Google Analytics.')
@ -139,7 +129,6 @@ export function getTriggers(type: WebhookType) {
switch(type) {
case WebhookType.TuneIn:
case WebhookType.RadioDe:
case WebhookType.GoogleAnalyticsV3:
case WebhookType.GoogleAnalyticsV4:
case WebhookType.MatomoAnalytics:
return [];
@ -150,7 +139,6 @@ export function getTriggers(type: WebhookType) {
case WebhookType.Discord:
case WebhookType.Telegram:
case WebhookType.Twitter:
case WebhookType.Mastodon:
default:
return allTriggersExceptListeners;

View File

@ -20,24 +20,31 @@
:fields="fields"
:api-url="listUrl"
>
<template #cell(name)="row">
<template #cell(name)="{item}">
<div class="typography-subheading">
{{ row.item.name }}
{{ item.name }}
</div>
{{ getWebhookName(row.item.type) }}
<div
v-if="!row.item.is_enabled"
class="badge bg-danger"
>
{{ $gettext('Disabled') }}
</div>
</template>
<template #cell(triggers)="row">
<template v-if="row.item.triggers.length > 0">
<template v-if="isWebhookSupported(item.type)">
{{ getWebhookName(item.type) }}
<div
v-for="(name, index) in getTriggerNames(row.item.triggers)"
:key="row.item.id+'_'+index"
v-if="!item.is_enabled"
class="badge bg-danger"
>
{{ $gettext('Disabled') }}
</div>
</template>
<template v-else>
{{
$gettext('This web hook is no longer supported. Removing it is recommended.')
}}
</template>
</template>
<template #cell(triggers)="{item}">
<template v-if="isWebhookSupported(item.type) && item.triggers.length > 0">
<div
v-for="(name, index) in getTriggerNames(item.triggers)"
:key="item.id+'_'+index"
class="small"
>
{{ name }}
@ -47,34 +54,36 @@
&nbsp;
</template>
</template>
<template #cell(actions)="row">
<template #cell(actions)="{item}">
<div class="btn-group btn-group-sm">
<button
type="button"
class="btn btn-primary"
@click="doEdit(row.item.links.self)"
>
{{ $gettext('Edit') }}
</button>
<button
type="button"
class="btn"
:class="getToggleVariant(row.item)"
@click="doToggle(row.item.links.toggle)"
>
{{ langToggleButton(row.item) }}
</button>
<button
type="button"
class="btn btn-secondary"
@click="doTest(row.item.links.test)"
>
{{ $gettext('Test') }}
</button>
<template v-if="isWebhookSupported(item.type)">
<button
type="button"
class="btn btn-primary"
@click="doEdit(item.links.self)"
>
{{ $gettext('Edit') }}
</button>
<button
type="button"
class="btn"
:class="getToggleVariant(item)"
@click="doToggle(item.links.toggle)"
>
{{ langToggleButton(item) }}
</button>
<button
type="button"
class="btn btn-secondary"
@click="doTest(item.links.test)"
>
{{ $gettext('Test') }}
</button>
</template>
<button
type="button"
class="btn btn-danger"
@click="doDelete(row.item.links.self)"
@click="doDelete(item.links.self)"
>
{{ $gettext('Delete') }}
</button>
@ -140,6 +149,10 @@ const getToggleVariant = (record) => {
: 'btn-success';
};
const isWebhookSupported = (key) => {
return (key in langTypeDetails);
}
const getWebhookName = (key) => {
return get(langTypeDetails, [key, 'title'], '');
};

View File

@ -39,8 +39,6 @@ import Email from "./Form/Email.vue";
import Tunein from "./Form/Tunein.vue";
import Discord from "./Form/Discord.vue";
import Telegram from "./Form/Telegram.vue";
import Twitter from "./Form/Twitter.vue";
import GoogleAnalyticsV3 from "./Form/GoogleAnalyticsV3.vue";
import GoogleAnalyticsV4 from "./Form/GoogleAnalyticsV4.vue";
import MatomoAnalytics from "./Form/MatomoAnalytics.vue";
import Mastodon from "./Form/Mastodon.vue";
@ -83,9 +81,7 @@ const webhookComponents = {
[WebhookType.RadioDe]: RadioDe,
[WebhookType.Discord]: Discord,
[WebhookType.Telegram]: Telegram,
[WebhookType.Twitter]: Twitter,
[WebhookType.Mastodon]: Mastodon,
[WebhookType.GoogleAnalyticsV3]: GoogleAnalyticsV3,
[WebhookType.GoogleAnalyticsV4]: GoogleAnalyticsV4,
[WebhookType.MatomoAnalytics]: MatomoAnalytics,
};

View File

@ -1,52 +0,0 @@
<template>
<tab
:label="title"
:item-header-class="tabClass"
>
<div class="row g-3">
<form-group-field
id="form_config_tracking_id"
class="col-md-12"
:field="v$.config.tracking_id"
:label="$gettext('GA Property Tracking ID')"
:description="$gettext('The property ID used to track live listeners.')"
/>
</div>
</tab>
</template>
<script setup lang="ts">
import FormGroupField from "~/components/Form/FormGroupField.vue";
import {useVModel} from "@vueuse/core";
import {useVuelidateOnFormTab} from "~/functions/useVuelidateOnFormTab";
import {required} from "@vuelidate/validators";
import Tab from "~/components/Common/Tab.vue";
const props = defineProps({
title: {
type: String,
required: true
},
form: {
type: Object,
required: true
}
});
const emit = defineEmits(['update:form']);
const form = useVModel(props, 'form', emit);
const {v$, tabClass} = useVuelidateOnFormTab(
{
config: {
tracking_id: {required}
}
},
form,
{
config: {
tracking_id: ''
}
}
);
</script>

View File

@ -1,120 +0,0 @@
<template>
<tab
:label="title"
:item-header-class="tabClass"
>
<form-markup id="twitter_account_details">
<template #label>
{{ $gettext('Twitter Account Details') }}
</template>
<p class="card-text">
{{ $gettext('Steps for configuring a Twitter application:') }}
</p>
<ul>
<li>
{{
$gettext('Create a new app on the Twitter Applications site. Use this installation\'s base URL as the application URL.')
}}
<br>
<a
href="https://developer.twitter.com/en/apps"
target="_blank"
>
{{ $gettext('Twitter Applications') }}
</a>
</li>
<li>
{{ $gettext('In the newly created application, click the "Keys and Access Tokens" tab.') }}
</li>
<li>
{{ $gettext('At the bottom of the page, click "Create my access token".') }}
</li>
</ul>
<p class="card-text">
{{
$gettext('Once these steps are completed, enter the information from the "Keys and Access Tokens" page into the fields below.')
}}
</p>
</form-markup>
<div class="row g-3 mb-3">
<form-group-field
id="form_config_consumer_key"
class="col-md-6"
:field="v$.config.consumer_key"
:label="$gettext('Consumer Key (API Key)')"
/>
<form-group-field
id="form_config_consumer_secret"
class="col-md-6"
:field="v$.config.consumer_secret"
:label="$gettext('Consumer Secret (API Secret)')"
/>
<form-group-field
id="form_config_token"
class="col-md-6"
:field="v$.config.token"
:label="$gettext('Access Token')"
/>
<form-group-field
id="form_config_token_secret"
class="col-md-6"
:field="v$.config.token_secret"
:label="$gettext('Access Token Secret')"
/>
<common-rate-limit-fields v-model:form="form" />
</div>
<common-social-post-fields v-model:form="form" />
</tab>
</template>
<script setup lang="ts">
import FormGroupField from "~/components/Form/FormGroupField.vue";
import CommonRateLimitFields from "./Common/RateLimitFields.vue";
import CommonSocialPostFields from "./Common/SocialPostFields.vue";
import FormMarkup from "~/components/Form/FormMarkup.vue";
import {useVModel} from "@vueuse/core";
import {useVuelidateOnFormTab} from "~/functions/useVuelidateOnFormTab";
import {required} from "@vuelidate/validators";
import Tab from "~/components/Common/Tab.vue";
const props = defineProps({
title: {
type: String,
required: true
},
form: {
type: Object,
required: true
}
});
const emit = defineEmits(['update:form']);
const form = useVModel(props, 'form', emit);
const {v$, tabClass} = useVuelidateOnFormTab(
{
config: {
consumer_key: {required},
consumer_secret: {required},
token: {required},
token_secret: {required},
}
},
form,
{
config: {
consumer_key: '',
consumer_secret: '',
token: '',
token_secret: '',
}
}
);
</script>

View File

@ -15,8 +15,7 @@
:types="buildTypeInfo([
WebhookType.Discord,
WebhookType.Telegram,
WebhookType.Mastodon,
WebhookType.Twitter
WebhookType.Mastodon
])"
@select="selectType"
/>
@ -34,7 +33,6 @@
<type-select-section
:title="$gettext('Analytics')"
:types="buildTypeInfo([
WebhookType.GoogleAnalyticsV3,
WebhookType.GoogleAnalyticsV4,
WebhookType.MatomoAnalytics
])"

View File

@ -1,67 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Webhook\Connector;
use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station;
use App\Entity\StationWebhook;
use TheIconic\Tracking\GoogleAnalytics\Analytics;
use TheIconic\Tracking\GoogleAnalytics\Network\HttpClient;
final class GoogleAnalyticsV3 extends AbstractGoogleAnalyticsConnector
{
/**
* @inheritDoc
*/
public function dispatch(
Station $station,
StationWebhook $webhook,
NowPlaying $np,
array $triggers
): void {
$config = $webhook->getConfig();
if (empty($config['tracking_id'])) {
throw $this->incompleteConfigException($webhook);
}
// Get listen URLs for each mount point.
$listenUrls = $this->buildListenUrls($station);
// Build analytics
$httpClient = new HttpClient();
$httpClient->setClient($this->httpClient);
$analytics = new Analytics(true);
$analytics->setHttpClient($httpClient);
$analytics->setProtocolVersion('1')
->setTrackingId($config['tracking_id']);
// Get all current listeners
$liveListeners = $this->listenerRepo->iterateLiveListenersArray($station);
$i = 0;
foreach ($liveListeners as $listener) {
$listenerUrl = $this->getListenUrl($listener, $listenUrls);
if (null === $listenerUrl) {
continue;
}
$analytics->setClientId($listener['listener_uid']);
$analytics->setUserAgentOverride($listener['listener_user_agent']);
$analytics->setIpOverride($listener['listener_ip']);
$analytics->setDocumentPath($listenerUrl);
$analytics->__call('enqueuePageView', []);
$i++;
if (20 === $i) {
$analytics->sendEnqueuedHits();
$i = 0;
}
}
$analytics->sendEnqueuedHits();
}
}

View File

@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Webhook\Connector;
use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station;
use App\Entity\StationWebhook;
use App\Service\GuzzleFactory;
use GuzzleHttp\Client;
use GuzzleHttp\Subscriber\Oauth\Oauth1;
final class Twitter extends AbstractSocialConnector
{
public function __construct(
Client $httpClient,
private readonly GuzzleFactory $guzzleFactory,
) {
parent::__construct($httpClient);
}
protected function getRateLimitTime(StationWebhook $webhook): ?int
{
$config = $webhook->getConfig();
$rateLimitSeconds = (int)($config['rate_limit'] ?? 0);
return max(10, $rateLimitSeconds);
}
/**
* @inheritDoc
*/
public function dispatch(
Station $station,
StationWebhook $webhook,
NowPlaying $np,
array $triggers
): void {
$config = $webhook->getConfig();
if (
empty($config['consumer_key'])
|| empty($config['consumer_secret'])
|| empty($config['token'])
|| empty($config['token_secret'])
) {
throw $this->incompleteConfigException($webhook);
}
// Set up Twitter OAuth
$stack = clone $this->guzzleFactory->getHandlerStack();
$middleware = new Oauth1(
[
'consumer_key' => trim($config['consumer_key']),
'consumer_secret' => trim($config['consumer_secret']),
'token' => trim($config['token']),
'token_secret' => trim($config['token_secret']),
]
);
$stack->push($middleware);
// Dispatch webhook
$this->logger->debug('Posting to Twitter...');
foreach ($this->getMessages($webhook, $np, $triggers) as $message) {
$messageBody = [
'status' => $message,
];
$response = $this->httpClient->request(
'POST',
'https://api.twitter.com/1.1/statuses/update.json',
[
'auth' => 'oauth',
'handler' => $stack,
'form_params' => $messageBody,
]
);
$this->logHttpResponse(
$webhook,
$response,
$messageBody
);
}
}
}

View File

@ -17,6 +17,7 @@ use App\Webhook\Connector\AbstractConnector;
use App\Webhook\Enums\WebhookTriggers;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use RuntimeException;
use Throwable;
final class Dispatcher
@ -87,6 +88,16 @@ final class Dispatcher
$webhookType = $webhook->getType();
$webhookClass = $webhookType->getClass();
if (null === $webhookClass) {
$this->logger->error(
sprintf(
'Webhook type "%s" is no longer supported. Removing this webhook is recommended.',
$webhookType->value
)
);
continue;
}
$this->logger->debug(
sprintf('Dispatching connector "%s".', $webhookType->value)
);
@ -155,6 +166,12 @@ final class Dispatcher
$webhookType = $webhook->getType();
$webhookClass = $webhookType->getClass();
if (null === $webhookClass) {
throw new RuntimeException(
'Webhook type is no longer supported. Removing this webhook is recommended.'
);
}
/** @var AbstractConnector $webhookObj */
$webhookObj = $this->di->get($webhookClass);
$webhookObj->dispatch($station, $webhook, $np, [

View File

@ -7,14 +7,12 @@ namespace App\Webhook\Enums;
use App\Webhook\Connector\Discord;
use App\Webhook\Connector\Email;
use App\Webhook\Connector\Generic;
use App\Webhook\Connector\GoogleAnalyticsV3;
use App\Webhook\Connector\GoogleAnalyticsV4;
use App\Webhook\Connector\Mastodon;
use App\Webhook\Connector\MatomoAnalytics;
use App\Webhook\Connector\RadioDe;
use App\Webhook\Connector\Telegram;
use App\Webhook\Connector\TuneIn;
use App\Webhook\Connector\Twitter;
enum WebhookTypes: string
{
@ -26,17 +24,19 @@ enum WebhookTypes: string
case Discord = 'discord';
case Telegram = 'telegram';
case Twitter = 'twitter';
case Mastodon = 'mastodon';
case GoogleAnalyticsV3 = 'google_analytics';
case GoogleAnalyticsV4 = 'google_analytics_v4';
case MatomoAnalytics = 'matomo_analytics';
// Retired connectors
case Twitter = 'twitter';
case GoogleAnalyticsV3 = 'google_analytics';
/**
* @return class-string
* @return class-string|null
*/
public function getClass(): string
public function getClass(): ?string
{
return match ($this) {
self::Generic => Generic::class,
@ -45,11 +45,10 @@ enum WebhookTypes: string
self::RadioDe => RadioDe::class,
self::Discord => Discord::class,
self::Telegram => Telegram::class,
self::Twitter => Twitter::class,
self::Mastodon => Mastodon::class,
self::GoogleAnalyticsV3 => GoogleAnalyticsV3::class,
self::GoogleAnalyticsV4 => GoogleAnalyticsV4::class,
self::MatomoAnalytics => MatomoAnalytics::class
self::MatomoAnalytics => MatomoAnalytics::class,
default => null
};
}
}