mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
28 changed files with 566 additions and 9 deletions
@ -0,0 +1,26 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Rules.Actions |
|||
{ |
|||
[TypeName(nameof(TweetAction))] |
|||
public sealed class TweetAction : RuleAction |
|||
{ |
|||
public string AccessToken { get; set; } |
|||
|
|||
public string AccessSecret { get; set; } |
|||
|
|||
public string Text { get; set; } |
|||
|
|||
public override T Accept<T>(IRuleActionVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Rules.Actions |
|||
{ |
|||
public sealed class TwitterOptions |
|||
{ |
|||
public string ClientId { get; set; } |
|||
|
|||
public string ClientSecret { get; set; } |
|||
|
|||
public bool IsConfigured() |
|||
{ |
|||
return !string.IsNullOrWhiteSpace(ClientId) && !string.IsNullOrWhiteSpace(ClientSecret); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using CoreTweet; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable SA1649 // File name must match first type name
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.HandleRules.Actions |
|||
{ |
|||
public sealed class TweetJob |
|||
{ |
|||
public string AccessToken { get; set; } |
|||
|
|||
public string AccessSecret { get; set; } |
|||
|
|||
public string Text { get; set; } |
|||
} |
|||
|
|||
public sealed class TweetActionHandler : RuleActionHandler<TweetAction, TweetJob> |
|||
{ |
|||
private const string Description = "Send a tweet"; |
|||
|
|||
private readonly RuleEventFormatter formatter; |
|||
private readonly TwitterOptions twitterOptions; |
|||
|
|||
public TweetActionHandler(RuleEventFormatter formatter, IOptions<TwitterOptions> twitterOptions) |
|||
{ |
|||
Guard.NotNull(formatter, nameof(formatter)); |
|||
Guard.NotNull(twitterOptions, nameof(twitterOptions)); |
|||
|
|||
this.formatter = formatter; |
|||
|
|||
this.twitterOptions = twitterOptions.Value; |
|||
} |
|||
|
|||
protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) |
|||
{ |
|||
var text = formatter.Format(action.Text, @event); |
|||
|
|||
var ruleJob = new TweetJob |
|||
{ |
|||
Text = text, |
|||
AccessToken = action.AccessToken, |
|||
AccessSecret = action.AccessSecret |
|||
}; |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(TweetJob job) |
|||
{ |
|||
try |
|||
{ |
|||
var tokens = Tokens.Create( |
|||
twitterOptions.ClientId, |
|||
twitterOptions.ClientSecret, |
|||
job.AccessToken, |
|||
job.AccessSecret); |
|||
|
|||
var response = await tokens.Statuses.UpdateAsync(status => job.Text); |
|||
|
|||
return ($"Tweeted: {job.Text}", null); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return (ex.Message, ex); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using NJsonSchema.Annotations; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Rules.Models.Actions |
|||
{ |
|||
[JsonSchema("Tweet")] |
|||
public sealed class TweetActionDto : RuleActionDto |
|||
{ |
|||
/// <summary>
|
|||
/// The access token.
|
|||
/// </summary>
|
|||
[Required] |
|||
public string AccessToken { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The access secret.
|
|||
/// </summary>
|
|||
[Required] |
|||
public string AccessSecret { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The text that is sent as tweet to twitter.
|
|||
/// </summary>
|
|||
public string Text { get; set; } |
|||
|
|||
public override RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new TweetAction()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using static CoreTweet.OAuth; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Rules |
|||
{ |
|||
public sealed class TwitterController : Controller |
|||
{ |
|||
private readonly TwitterOptions twitterOptions; |
|||
|
|||
public TwitterController(IOptions<TwitterOptions> twitterOptions) |
|||
{ |
|||
this.twitterOptions = twitterOptions.Value; |
|||
} |
|||
|
|||
public sealed class TokenRequest |
|||
{ |
|||
public string PinCode { get; set; } |
|||
|
|||
public string RequestToken { get; set; } |
|||
|
|||
public string RequestTokenSecret { get; set; } |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("rules/twitter/auth")] |
|||
public async Task<IActionResult> Auth() |
|||
{ |
|||
var session = await AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret); |
|||
|
|||
return Ok(new |
|||
{ |
|||
session.AuthorizeUri, |
|||
session.RequestToken, |
|||
session.RequestTokenSecret |
|||
}); |
|||
} |
|||
|
|||
[HttpPost] |
|||
[Route("rules/twitter/token")] |
|||
public async Task<IActionResult> AuthComplete([FromBody] TokenRequest request) |
|||
{ |
|||
var session = new OAuthSession |
|||
{ |
|||
ConsumerKey = twitterOptions.ClientId, |
|||
ConsumerSecret = twitterOptions.ClientSecret, |
|||
RequestToken = request.RequestToken, |
|||
RequestTokenSecret = request.RequestTokenSecret |
|||
}; |
|||
|
|||
var tokens = await session.GetTokensAsync(request.PinCode); |
|||
|
|||
return Ok(new |
|||
{ |
|||
tokens.AccessToken, |
|||
tokens.AccessTokenSecret |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<h3 class="wizard-title">Tweet a status update to your twitter feed</h3> |
|||
|
|||
<form [formGroup]="actionForm" class="form-horizontal"> |
|||
<div class="form-group row"> |
|||
<div class="col col-9 offset-3"> |
|||
<ng-container *ngIf="!isRedirected"> |
|||
<button class="btn btn-twitter" [disabled]="isAuthenticating" (click)="auth()"> |
|||
Request access token with twitter |
|||
</button> |
|||
</ng-container> |
|||
|
|||
<ng-container *ngIf="isRedirected"> |
|||
<form class="form-inline" (ngSubmit)="complete()"> |
|||
<input class="form-control mr-1" [(ngModel)]="pinCode" [ngModelOptions]="{ standalone: true }" placeholder="Pin" /> |
|||
|
|||
<button type="submit" class="btn btn-secondary" [disabled]="!pinCode"> |
|||
Complete |
|||
</button> |
|||
</form> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-3 col-form-label" for="accessToken">Access Token</label> |
|||
|
|||
<div class="col col-9"> |
|||
<sqx-control-errors for="accessToken" submitOnly="true" [submitted]="actionFormSubmitted"></sqx-control-errors> |
|||
|
|||
<input type="text" readonly class="form-control" id="accessToken" formControlName="accessToken" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-3 col-form-label" for="accessToken">Access Secret</label> |
|||
|
|||
<div class="col col-9"> |
|||
<sqx-control-errors for="accessSecret" submitOnly="true" [submitted]="actionFormSubmitted"></sqx-control-errors> |
|||
|
|||
<input type="text" readonly class="form-control" id="accessSecret" formControlName="accessSecret" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-3 col-form-label" for="text">Text</label> |
|||
|
|||
<div class="col col-9"> |
|||
<sqx-control-errors for="text" [submitted]="actionFormSubmitted"></sqx-control-errors> |
|||
|
|||
<textarea class="form-control" id="text" formControlName="text"></textarea> |
|||
|
|||
<small class="form-text text-muted"> |
|||
The text to tweet. Read the <a routerLink="help">help</a> section for information about advanced formatting. |
|||
</small> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,2 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
@ -0,0 +1,97 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Component, Input, OnInit } from '@angular/core'; |
|||
import { FormControl, FormGroup, Validators } from '@angular/forms'; |
|||
|
|||
import { DialogService } from '@app/shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-tweet-action', |
|||
styleUrls: ['./tweet-action.component.scss'], |
|||
templateUrl: './tweet-action.component.html' |
|||
}) |
|||
export class TweetActionComponent implements OnInit { |
|||
private request: any; |
|||
|
|||
@Input() |
|||
public action: any; |
|||
|
|||
@Input() |
|||
public actionForm: FormGroup; |
|||
|
|||
@Input() |
|||
public actionFormSubmitted = false; |
|||
|
|||
public isAuthenticating = false; |
|||
public isRedirected = false; |
|||
|
|||
public pinCode: string; |
|||
|
|||
constructor( |
|||
private readonly dialogs: DialogService, |
|||
private readonly httpClient: HttpClient |
|||
) { |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.actionForm.setControl('accessToken', |
|||
new FormControl(this.action.accessToken || '', [ |
|||
Validators.required |
|||
])); |
|||
|
|||
this.actionForm.setControl('accessSecret', |
|||
new FormControl(this.action.accessSecret || '', [ |
|||
Validators.required |
|||
])); |
|||
|
|||
this.actionForm.setControl('text', |
|||
new FormControl(this.action.text || '', [ |
|||
Validators.required |
|||
])); |
|||
} |
|||
|
|||
public auth() { |
|||
this.isAuthenticating = true; |
|||
|
|||
this.httpClient.get('api/rules/twitter/auth') |
|||
.subscribe((response: any) => { |
|||
this.request = { |
|||
requestToken: response.requestToken, |
|||
requestTokenSecret: response.requestTokenSecret |
|||
}; |
|||
|
|||
this.isAuthenticating = false; |
|||
this.isRedirected = true; |
|||
|
|||
window.open(response.authorizeUri, '_blank'); |
|||
}, () => { |
|||
this.dialogs.notifyError('Failed to authenticate with twitter.'); |
|||
|
|||
this.isAuthenticating = false; |
|||
this.isRedirected = false; |
|||
}); |
|||
} |
|||
|
|||
public complete() { |
|||
this.request.pinCode = this.pinCode; |
|||
|
|||
this.httpClient.post('api/rules/twitter/token', this.request) |
|||
.subscribe((response: any) => { |
|||
this.actionForm.get('accessToken')!.setValue(response.accessToken); |
|||
this.actionForm.get('accessSecret')!.setValue(response.accessTokenSecret); |
|||
|
|||
this.isRedirected = false; |
|||
}, () => { |
|||
this.dialogs.notifyError('Failed to request access token.'); |
|||
|
|||
this.isAuthenticating = false; |
|||
this.isRedirected = false; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FluentAssertions; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions |
|||
{ |
|||
public class TweetActionTests |
|||
{ |
|||
[Fact] |
|||
public async Task Should_add_error_if_access_token_is_null() |
|||
{ |
|||
var action = new TweetAction { AccessToken = null, AccessSecret = "secret" }; |
|||
|
|||
var errors = await RuleActionValidator.ValidateAsync(action); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new List<ValidationError> |
|||
{ |
|||
new ValidationError("Access Token is required.", "AccessToken") |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_access_secret_is_null() |
|||
{ |
|||
var action = new TweetAction { AccessToken = "token", AccessSecret = null }; |
|||
|
|||
var errors = await RuleActionValidator.ValidateAsync(action); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new List<ValidationError> |
|||
{ |
|||
new ValidationError("Access Secret is required.", "AccessSecret") |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_access_token_and_secret_defined() |
|||
{ |
|||
var action = new TweetAction { AccessToken = "token", AccessSecret = "secret" }; |
|||
|
|||
var errors = await RuleActionValidator.ValidateAsync(action); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue