Browse Source

Merge pull request #314 from Squidex/feature-twitter

Feature twitter
pull/315/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
cad956d0c0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs
  2. 21
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TwitterOptions.cs
  3. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
  4. 80
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs
  5. 15
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs
  6. 1
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  7. 17
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs
  8. 7
      src/Squidex/AppServices.cs
  9. 4
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  10. 2
      src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs
  11. 41
      src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs
  12. 5
      src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs
  13. 69
      src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs
  14. 5
      src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  15. 10
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  16. 3
      src/Squidex/Config/Domain/RuleServices.cs
  17. 3
      src/Squidex/app/features/rules/declarations.ts
  18. 2
      src/Squidex/app/features/rules/module.ts
  19. 57
      src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html
  20. 2
      src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss
  21. 97
      src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts
  22. 7
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  23. 3
      src/Squidex/app/shared/services/rules.service.ts
  24. 19
      src/Squidex/app/theme/_bootstrap.scss
  25. 5
      src/Squidex/app/theme/_rules.scss
  26. 2
      src/Squidex/app/theme/_vars.scss
  27. 11
      src/Squidex/appsettings.json
  28. 57
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/TweetActionTests.cs

26
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs

@ -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);
}
}
}

21
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TwitterOptions.cs

@ -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);
}
}
}

2
src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs

@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Core.Rules
T Visit(SlackAction action);
T Visit(TweetAction action);
T Visit(WebhookAction action);
}
}

80
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs

@ -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);
}
}
}
}

15
src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
@ -17,18 +18,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30);
private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly Func<TKey, TClient> factory;
private readonly Func<TKey, Task<TClient>> factory;
public ClientPool(Func<TKey, TClient> factory)
{
this.factory = x => Task.FromResult(factory(x));
}
public ClientPool(Func<TKey, Task<TClient>> factory)
{
this.factory = factory;
}
public TClient GetClient(TKey key)
{
return GetClientAsync(key).Result;
}
public async Task<TClient> GetClientAsync(TKey key)
{
if (!memoryCache.TryGetValue<TClient>(key, out var client))
{
client = factory(key);
client = await factory(key);
memoryCache.Set(key, client, TTL);
}

1
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.1.0" />
<PackageReference Include="CoreTweet" Version="0.9.0.415" />
<PackageReference Include="Elasticsearch.Net" Version="6.2.0" />
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.0" />

17
src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs

@ -129,6 +129,23 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
return Task.FromResult<IEnumerable<ValidationError>>(errors);
}
public Task<IEnumerable<ValidationError>> Visit(TweetAction action)
{
var errors = new List<ValidationError>();
if (string.IsNullOrWhiteSpace(action.AccessToken))
{
errors.Add(new ValidationError("Access Token is required.", nameof(action.AccessToken)));
}
if (string.IsNullOrWhiteSpace(action.AccessSecret))
{
errors.Add(new ValidationError("Access Secret is required.", nameof(action.AccessSecret)));
}
return Task.FromResult<IEnumerable<ValidationError>>(errors);
}
public Task<IEnumerable<ValidationError>> Visit(SlackAction action)
{
var errors = new List<ValidationError>();

7
src/Squidex/AppServices.cs

@ -14,6 +14,7 @@ using Squidex.Config;
using Squidex.Config.Authentication;
using Squidex.Config.Domain;
using Squidex.Config.Web;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Infrastructure.Commands;
namespace Squidex
@ -44,7 +45,11 @@ namespace Squidex
services.Configure<ReadonlyOptions>(
config.GetSection("mode"));
services.Configure<ContentsControllerOptions>(
services.Configure<TwitterOptions>(
config.GetSection("twitter"));
services.Configure<MyContentsControllerOptions>(
config.GetSection("contentsController"));
services.Configure<MyUrlsOptions>(
config.GetSection("urls"));

4
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -30,14 +30,14 @@ namespace Squidex.Areas.Api.Controllers.Contents
[SwaggerIgnore]
public sealed class ContentsController : ApiController
{
private readonly IOptions<ContentsControllerOptions> controllerOptions;
private readonly IOptions<MyContentsControllerOptions> controllerOptions;
private readonly IContentQueryService contentQuery;
private readonly IGraphQLService graphQl;
public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery,
IGraphQLService graphQl,
IOptions<ContentsControllerOptions> controllerOptions)
IOptions<MyContentsControllerOptions> controllerOptions)
: base(commandBus)
{
this.contentQuery = contentQuery;

2
src/Squidex/Areas/Api/Controllers/Contents/ContentsControllerOptions.cs → src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs

@ -7,7 +7,7 @@
namespace Squidex.Areas.Api.Controllers.Contents
{
public sealed class ContentsControllerOptions
public sealed class MyContentsControllerOptions
{
public bool EnableSurrogateKeys { get; set; }

41
src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs

@ -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());
}
}
}

5
src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs

@ -55,6 +55,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters
return SimpleMapper.Map(action, new SlackActionDto());
}
public RuleActionDto Visit(TweetAction action)
{
return SimpleMapper.Map(action, new TweetActionDto());
}
public RuleActionDto Visit(WebhookAction action)
{
return SimpleMapper.Map(action, new WebhookActionDto());

69
src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs

@ -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
});
}
}
}

5
src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs

@ -22,5 +22,10 @@ namespace Squidex.Areas.Api.Controllers.UI.Models
/// </summary>
[Required]
public string MapKey { get; set; }
/// <summary>
/// Indicates whether twitter actions are supported.
/// </summary>
public bool SupportsTwitterActions { get; set; }
}
}

10
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -8,11 +8,11 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using NSwag.Annotations;
using Orleans;
using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Config;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
@ -29,13 +29,18 @@ namespace Squidex.Areas.Api.Controllers.UI
public sealed class UIController : ApiController
{
private readonly MyUIOptions uiOptions;
private readonly TwitterOptions twitterOptions;
private readonly IGrainFactory grainFactory;
public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IGrainFactory grainFactory)
public UIController(ICommandBus commandBus,
IOptions<MyUIOptions> uiOptions,
IOptions<TwitterOptions> twitterOptions,
IGrainFactory grainFactory)
: base(commandBus)
{
this.uiOptions = uiOptions.Value;
this.grainFactory = grainFactory;
this.twitterOptions = twitterOptions.Value;
}
/// <summary>
@ -56,6 +61,7 @@ namespace Squidex.Areas.Api.Controllers.UI
result.Value["mapType"] = uiOptions.Map?.Type ?? "OSM";
result.Value["mapKey"] = uiOptions.Map?.GoogleMaps?.Key;
result.Value["supportTwitterAction"] = twitterOptions.IsConfigured();
return Ok(result.Value);
}

3
src/Squidex/Config/Domain/RuleServices.cs

@ -42,6 +42,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<MediumActionHandler>()
.As<IRuleActionHandler>();
services.AddSingletonAs<TweetActionHandler>()
.As<IRuleActionHandler>();
services.AddSingletonAs<SlackActionHandler>()
.As<IRuleActionHandler>();

3
src/Squidex/app/features/rules/declarations.ts

@ -11,9 +11,12 @@ export * from './pages/rules/actions/elastic-search-action.component';
export * from './pages/rules/actions/fastly-action.component';
export * from './pages/rules/actions/medium-action.component';
export * from './pages/rules/actions/slack-action.component';
export * from './pages/rules/actions/tweet-action.component';
export * from './pages/rules/actions/webhook-action.component';
export * from './pages/rules/triggers/asset-changed-trigger.component';
export * from './pages/rules/triggers/content-changed-trigger.component';
export * from './pages/rules/rule-wizard.component';
export * from './pages/rules/rules-page.component';

2
src/Squidex/app/features/rules/module.ts

@ -27,6 +27,7 @@ import {
RulesPageComponent,
RuleWizardComponent,
SlackActionComponent,
TweetActionComponent,
WebhookActionComponent
} from './declarations';
@ -69,6 +70,7 @@ const routes: Routes = [
RulesPageComponent,
RuleWizardComponent,
SlackActionComponent,
TweetActionComponent,
WebhookActionComponent
]
})

57
src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html

@ -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>

2
src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

97
src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts

@ -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;
});
}
}

7
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -114,6 +114,13 @@
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-slack-action>
</ng-container>
<ng-container *ngSwitchCase="'Tweet'">
<sqx-tweet-action
[action]="action"
[actionForm]="actionForm.form"
[actionFormSubmitted]="actionForm.submitted | async">
</sqx-tweet-action>
</ng-container>
<ng-container *ngSwitchCase="'Webhook'">
<sqx-webhook-action
[action]="action"

3
src/Squidex/app/shared/services/rules.service.ts

@ -49,6 +49,9 @@ export const ruleActions: any = {
'Slack': {
name: 'Send to Slack'
},
'Tweet': {
name: 'Tweet'
},
'Webhook': {
name: 'Send Webhook'
}

19
src/Squidex/app/theme/_bootstrap.scss

@ -264,6 +264,10 @@ a {
&-microsoft {
color: $color-extern-microsoft-icon;
}
&-twitter {
color: $color-extern-twitter-icon;
}
}
//
@ -304,6 +308,21 @@ a {
}
}
&-twitter {
@include button-variant($color-extern-twitter, $color-extern-twitter);
& {
color: $color-dark-foreground;
}
&:hover,
&:focus {
.icon-twitter {
color: darken($color-extern-twitter-icon, 5%);
}
}
}
// Special radio button.
&-radio {
& {

5
src/Squidex/app/theme/_rules.scss

@ -11,6 +11,7 @@ $action-slack: #5c3a58;
$action-azure: #55b3ff;
$action-fastly: #e23335;
$action-medium: #00ab6c;
$action-twitter: #1da1f2;
// sass-lint:disable class-name-format
@ -88,6 +89,10 @@ $action-medium: #00ab6c;
@include build-element($action-slack);
}
.rule-element-Tweet {
@include build-element($action-twitter);
}
.rule-element-Webhook {
@include build-element($action-webhook);
}

2
src/Squidex/app/theme/_vars.scss

@ -19,6 +19,8 @@ $color-extern-microsoft: #004185;
$color-extern-microsoft-icon: #1b67b7;
$color-extern-github: #191919;
$color-extern-github-icon: #4a4a4a;
$color-extern-twitter: #1da1f2;
$color-extern-twitter-icon: #1681bf;
$color-theme-blue: #3389ff;
$color-theme-blue-dark: #297ff6;

11
src/Squidex/appsettings.json

@ -247,5 +247,16 @@
* The url to you privacy statements, if you host squidex by yourself.
*/
"privacyUrl": "https://squidex.io/privacy"
},
"twitter": {
/*
* The client id for twitter.
*/
"clientId": "QZhb3HQcGCvE6G8yNNP9ksNet",
/*
* The client secret for twitter.
*/
"clientSecret": "Pdu9wdN72T33KJRFdFy1w4urBKDRzIyuKpc0OItQC2E616DuZD"
}
}

57
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/TweetActionTests.cs

@ -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…
Cancel
Save