Verificação de veracidade de mídias sociais
2012 - atual
github.com/meedan/checkdesk
github.com/meedan/check-api
github.com/meedan/check-web
github.com/meedan/lapis
github.com/meedan/generator-keefer
Backend
Frontend
Ruby On Rails template
Yeoman generator
Pender
github.com/meedan/pender
Check API
github.com/meedan/check-api
Alegre
github.com/meedan/alegre
Backend (Ruby On Rails APIs)
Check Web
github.com/meedan/check-web
Check Mark
github.com/meedan/check-mark
Check Slack Bot
github.com/meedan/check-bot
Frontend (JavaScript / React.js apps)
graphql
react.js
relay
ruby on rails
graphql
react.js
relay
ruby on rails
BACKEND
FRONTEND
GraphQL
Media
Comment
Tag
1
*
User
*
*
*
1
GET /api/v1/medias/1
GET /api/v1/medias/1/comments
GET /api/v1/medias/1/tags
GET /api/v1/medias/1/comments/1
GET /api/v1/users/1?fields=avatar,name
GET /api/v1/users/2?fields=avatar,name
GET /api/v1/users/3?fields=avatar,name
...
GET /api/v1/medias/1?include=comments&count=5
GET /api/v1/medias/1?include=comments,tags
&comments_count=5&tags_count=5
GET /api/v1/medias/1?fields=comments(text,date)
&tags(tag)
...
GET /api/v1/media_and_comments/1
GET /api/v1/media_comments_and_tags/1
GET /api/v1/media_comments_tags_and_users/1
GET /api/v1/medias/1?include=comments&count=5
GET /api/v1/medias/1?include=comments,tags
&comments_count=5&tags_count=5
GET /api/v1/medias/1?fields=comments(text,date)
&tags(tag)
...
GET /api/v1/media_and_comments/1
GET /api/v1/media_comments_and_tags/1
GET /api/v1/media_comments_tags_and_users/1
Muitas requisições!
POST /graphql
POST /api/graphql?query=
{
media(id: 1) {
title
embed
tags(first: 3) {
tag
}
comments(first: 5) {
created_at
text
user {
name,
avatar
}
}
}
}
POST /api/graphql?query=
{
media(id: 1) {
title
embed
tags(first: 3) {
tag
}
comments(first: 5) {
created_at
text
user {
name,
avatar
}
}
}
}
Media
Comment
Tag
1
*
User
*
*
*
1
~
POST /api/graphql?query=
{
media(id: 1) {
title
embed
tags(first: 3) {
tag
}
comments(first: 5) {
created_at
text
user {
name,
avatar
}
}
}
}
{
"media": {
"title": "Avangers Hulk Smash",
"embed": "<iframe src=\"...\"></iframe>",
"tags": [
{ "tag": "avengers" },
{ "tag": "operation" }
],
"comments": [
{
"text": "This is true",
"created_at": "2016-09-18 15:04:39",
"user": {
"name": "Ironman",
"avatar": "http://[...].png"
}
},
...
]
}
}
POST /api/graphql?query=
{
media(id: 1) {
title
embed
tags(first: 3) {
tag
}
comments(first: 5) {
created_at
text
user {
name,
avatar
}
}
}
}
Media
Comment
Tag
1
*
User
*
*
*
1
type QueryType {
media(id: Int): Media
comment(id: Int): Comment
user(id: Int): User
me: User
}
type Media {
title: String
embed: String
comments: [Comment]
tags:[Tag]
}
type Comment {
created_at: String
text: String
user: User
}
type User {
name: String
avatar: String
}
GraphQL
Ruby On Rails
$ git clone https://github.com/meedan/lapis.git
$ rails new <nome> -m <caminho para lapis_template.rb>
gem 'graphql'
gem 'graphql-relay'
Gemfile
class GraphqlController < BaseApiController
def create
vars = params[:variables] || {}
query = GraphQL::Query.new(RelayOnRailsSchema, params[:query], variables: vars)
render json: query.result
end
end
app/controllers/graphql_controller.rb (adicionar rota também)
RelayOnRailsSchema = GraphQL::Schema.new(query: QueryType, mutation: MutationType)
config/initializers/relay.rb
type Media {
title: String
embed: String
user: User
comments: [Comment]
}
# "Type" para diferenciar dos models
MediaType = GraphQL::ObjectType.define do
name 'Media'
description 'Media type'
field :title, types.String
field :embed, types.String
field :user do
type UserType
resolve -> (media, _args, _ctx) {
media.user
}
end
connection :comments, ->{ CommentType.connection_type }
do
argument :user_id, types.Int
resolve -> (media, args, ctx) {
media.comments(args[:user_id], ctx[:current_user])
}
end
end
app/graph/types/media_type.rb
# "Type" para diferenciar dos models
# MediaType = GraphQL::ObjectType.define do
# name 'Media'
# description 'Media type'
field :title, types.String
field :embed, types.String
# field :user do
# type UserType
# resolve -> (media, _args, _ctx) {
# media.user
# }
# end
# connection :comments, ->{ CommentType.connection_type }
# do
# argument :user_id, types.Int
#
# resolve -> (media, args, ctx) {
# media.comments(args[:user_id], ctx[:current_user])
# }
# end
# end
Por padrão, os campos
resolvem para métodos de instância
do model
# "Type" para diferenciar dos models
# MediaType = GraphQL::ObjectType.define do
# name 'Media'
# description 'Media type'
# field :title, types.String
# field :embed, types.String
field :user do
type UserType
resolve -> (media, _args, _ctx) {
media.user
}
end
# connection :comments, ->{ CommentType.connection_type }
# do
# argument :user_id, types.Int
#
# resolve -> (media, args, ctx) {
# media.comments(args[:user_id], ctx[:current_user])
# }
# end
# end
É possível ter um comportamento específico a partir de um método de resolução e referenciar outros tipos
# "Type" para diferenciar dos models
# MediaType = GraphQL::ObjectType.define do
# name 'Media'
# description 'Media type'
# field :title, types.String
# field :embed, types.String
# field :user do
# type UserType
# resolve -> (media, _args, _ctx) {
# media.user
# }
# end
connection :comments, ->{ CommentType.connection_type }
do
argument :user_id, types.Int
resolve -> (media, args, ctx) {
media.comments(args[:user_id], ctx[:current_user])
}
end
# end
Relacionamentos referenciam conexões de outros tipos, e podem receber argumentos e ter acesso ao contexto
mutation {
createMedia(
input: {
url: "http://youtu.be/7a_insd29fk"
clientMutationId: "1"
}
)
{
media {
id
}
}
}
Mutações provocam alterações na aplicação.
CRUD:
Queries: Read
Mutations:
# mutation {
createMedia(
# input: {
url: "http://youtu.be/7a_insd29fk"
# clientMutationId: "1"
# }
# )
{
media {
id
}
}
# }
Nome da mutação
Parâmetros de entrada
Formato de saída
CreateMedia = GraphQL::Relay::Mutation.define do
input_field :url, !types.String
return_field :media, MediaType
resolve -> (inputs, ctx) {
media = Media.new
media.url = inputs[:url]
media.save!
}
end
Em Rails, uma mutação deve definir os campos de entrada, saída e um método de execução
https://github.com/meedan/check-api/blob/develop/
app/graph/mutations/graphql_crud_operations.rb
module MediaMutations
Create, Update, Destroy =
GraphqlCrudOperations.define_crud_operations(
'media',
{ url: '!str' }, # Campos de criação
{ url: 'str', id: '!id' } # Campos de atualização
)
end
React.js
Controllers
Directives
Templates
Global Event Listeners
Models
var MediaController = new Controller({
addComment: function(comment) {
// Requisição REST
},
deleteComment: function(commentId) {
// Requisição REST
},
addTag: function(tag) {
// Requisição REST
}
});
var MediaController = new Controller({
addComment: function(comment) {
// Requisição REST
},
deleteComment: function(commentId) {
// Requisição REST
},
addTag: function(tag) {
// Requisição REST
}
});
<div class="media">
<h2>{{media.title}}</h2>
<ul class="tags">
<li ng-repeat="tag in tags">{{tag.tag}}</li>
<ul>
<ul class="comments">
<li ng-repeat="comment in comments">
<div ng-include="partials/comment.html">
</li>
<ul>
</div>
var MediaController = new Controller({
addComment: function(comment) {
// Requisição REST
},
deleteComment: function(commentId) {
// Requisição REST
},
addTag: function(tag) {
// Requisição REST
}
});
<div class="media">
<h2>{{media.title}}</h2>
<ul class="tags">
<li ng-repeat="tag in tags">{{tag.tag}}</li>
<ul>
<ul class="comments">
<li ng-repeat="comment in comments">
<div ng-include="partials/comment.html">
</li>
<ul>
</div>
this.$el.find('#delete-comment')
.on('click', function() {
// chama deleteComment
});
MediaComponent
CommentsComponent
TagsComponent
TagComponent
CommentComponent
CommentComponent
CommentComponent
x
import React, { Component, PropTypes } from 'react';
import Relay from 'react-relay';
import DeleteTagMutation from './DeleteTagMutation';
class Tag extends Component {
handleDelete() {
const id = this.props.tag.id;
Relay.Store.commitUpdate(
new DeleteTagMutation({
id: id
})
);
}
render() {
const tag = this.props.tag;
return (
<div className="tag">
<span>{tag.tag}</span>
<span onClick={this.delete.bind(this)}>
x
</span>
</div>
);
}
}
export default Tag;
Parece custoso? Mas é rápido!
Virtual DOM
// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
//
// class Tag extends Component {
// handleDelete() {
const id = this.props.tag.id;
this.setState({ deleted: true });
// Relay.Store.commitUpdate(
// new DeleteTagMutation({
// id: id
// })
// );
// }
//
// render() {
const tag = this.props.tag;
if (this.state.deleted) {
return (<span>Deleted</span>);
}
//
// componentWillMount() {
// this.sort();
// }
//
// return (
// <div className="tag">
// <span>{tag.tag}</span>
// <span onClick={this.delete.bind(this)}>
// x
// </span>
// </div>
// );
// }
// }
//
// export default Tag;
props
São imutáveis
state
É mutável
class Tags extends Component {
render() {
return (
<ul className="tags-list">
{props.tags.map(function(tag) {
return (
<Tag tag={tag}>
);
})}
</ul>
);
}
}
// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
//
// class Tag extends Component {
componentWillMount() {
this.parent.sort();
}
// render() {
// const tag = this.props.tag;
//
// if (this.state.deleted) {
// return (<span>Deleted</span>);
// }
//
// return (
// <div className="tag">
// <span>{tag.tag}</span>
// <span onClick={this.delete.bind(this)}>
// x
// </span>
// </div>
// );
// }
// }
//
// export default Tag;
Ciclo de vida
Callbacks que são chamados
em sequência na processo de construção/atualização
de um componente
class Tags extends Component {
render() {
return (
<ul className="tags-list">
{props.tags.map(function(tag) {
return (
<Tag tag={tag}>
);
})}
</ul>
);
}
}
Componente React
React.js
DOM
React Native
Android
iOS
$ git clone 'https://github.com
/meedan/generator-keefer.git'
$ cd generator-keefer
$ cp config.yml.example config.yml
$ npm install -g yo
$ npm link
$ yo keefer
$ PLATFORM=web npm run build
$ PLATFORM=chrome npm run build
$ PLATFORM=android npm run build
Relay
Relay
GraphQL
React.js
class MediaComponent extends Component {
render() {
const media = this.props.media;
return (
<div>
<article>
<MediaDetail media={media} />
<Tags tags={media.tags} />
<h3>Verification Timeline</h3>
<Comments comments={media.comments} />
</article>
</div>
);
}
}
class MediaComponent extends Component {
render() {
const media = this.props.media;
return (
<div>
<article>
<MediaDetail media={media} />
<h2>{media.title}</h2>
<Tags tags={media.tags} />
<h3>Verification Timeline</h3>
<Comments comments={media.comments} />
</article>
</div>
);
}
}
const MediaContainer = Relay.createContainer(
MediaComponent, {
fragments: {
media: () => Relay.QL`
fragment on Media {
title
embed
user { ...userFragment }
tags(first: 3) { edges { node {
tag, id
} } }
comments(first: 5) { edges { node {
...commentFragment
} } }
}
`
}
});
class Media extends Component {
render() {
var route = new MediaRoute({ id: 1 });
return (
<Relay.RootContainer
Component={MediaContainer}
route={route}
/>
);
}
}
class MediaRoute extends Relay.Route {
static queries = {
media: () => Relay.QL`query Media {
media(id: $id)
}`,
};
static paramDefinitions = {
id: { required: true }
};
static routeName = 'MediaRoute';
};
class CreateTagMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation createTag {
createTag
}`;
}
getFatQuery() {
return Relay.QL`fragment on CreateTagPayload
{ tagEdge, media { tags } }`;
}
getVariables() {
const media = this.props.media;
return { tag: media.tag.tag, media_id: media.id };
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'media',
parentID: this.props.media.id,
connectionName: 'tags',
edgeName: 'tagEdge',
rangeBehaviors: {
'': 'append'
}
}];
}
}
// class CreateTagMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation createTag {
createTag
}`;
}
//
// getFatQuery() {
// return Relay.QL`fragment on CreateTagPayload
// { tagEdge, media { tags } }`;
// }
//
// getVariables() {
// const media = this.props.media;
// return { tag: media.tag.tag, media_id: media.id };
// }
//
// getConfigs() {
// return [{
// type: 'RANGE_ADD',
// parentName: 'media',
// parentID: this.props.media.id,
// connectionName: 'tags',
// edgeName: 'tagEdge',
// rangeBehaviors: {
// '': 'append'
// }
// }];
// }
// }
Mutação a ser
chamada
Retorno esperado
Parâmetros de
entrada
Como atualizar o cache local
// class CreateTagMutation extends Relay.Mutation {
// getMutation() {
// return Relay.QL`mutation createTag {
// createTag
// }`;
// }
//
getFatQuery() {
return Relay.QL`fragment on CreateTagPayload
{ tagEdge, media { tags } }`;
}
//
// getVariables() {
// const media = this.props.media;
// return { tag: media.tag.tag, media_id: media.id };
// }
//
// getConfigs() {
// return [{
// type: 'RANGE_ADD',
// parentName: 'media',
// parentID: this.props.media.id,
// connectionName: 'tags',
// edgeName: 'tagEdge',
// rangeBehaviors: {
// '': 'append'
// }
// }];
// }
// }
Mutação a ser
chamada
Retorno esperado
Parâmetros de
entrada
Como atualizar o cache local
// class CreateTagMutation extends Relay.Mutation {
// getMutation() {
// return Relay.QL`mutation createTag {
// createTag
// }`;
// }
//
// getFatQuery() {
// return Relay.QL`fragment on CreateTagPayload
// { tagEdge, media { tags } }`;
// }
//
getVariables() {
const media = this.props.media;
return { tag: media.tag.tag, media_id: media.id };
}
//
// getConfigs() {
// return [{
// type: 'RANGE_ADD',
// parentName: 'media',
// parentID: this.props.media.id,
// connectionName: 'tags',
// edgeName: 'tagEdge',
// rangeBehaviors: {
// '': 'append'
// }
// }];
// }
// }
Mutação a ser
chamada
Retorno esperado
Parâmetros de
entrada
Como atualizar o cache local
// class CreateTagMutation extends Relay.Mutation {
// getMutation() {
// return Relay.QL`mutation createTag {
// createTag
// }`;
// }
//
// getFatQuery() {
// return Relay.QL`fragment on CreateTagPayload
// { tagEdge, media { tags } }`;
// }
//
// getVariables() {
// const media = this.props.media;
// return { tag: media.tag.tag, media_id: media.id };
// }
//
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'media',
parentID: this.props.media.id,
connectionName: 'tags',
edgeName: 'tagEdge',
rangeBehaviors: {
'': 'append'
}
}];
}
}
Mutação a ser
chamada
Retorno esperado
Parâmetros de
entrada
Como atualizar o cache local
// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
//
// class Tag extends Component {
// handleDelete() {
// const id = this.props.tag.id;
Relay.Store.commitUpdate(
new DeleteTagMutation({
id: id
})
);
// }
//
// render() {
// const tag = this.props.tag;
//
// return (
// <div className="tag">
// <span>{tag.tag}</span>
// <span onClick={this.delete.bind(this)}>
// x
// </span>
// </div>
// );
// }
// }
//
// export default Tag;
Chamandouma mutação
slides.com/caiosba/fisl18