Linkedin, Twitter Integration
This commit is contained in:
parent
78c0841d64
commit
22318b2eb6
0
erpnext/crm/doctype/linkedin_settings/__init__.py
Normal file
0
erpnext/crm/doctype/linkedin_settings/__init__.py
Normal file
66
erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
Normal file
66
erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('LinkedIn Settings', {
|
||||
onload: function(frm){
|
||||
if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||
frappe.confirm(
|
||||
'Session not valid, Do you want to login?',
|
||||
function(){
|
||||
frm.trigger("login");
|
||||
},
|
||||
function(){
|
||||
window.close();
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
refresh: function(frm){
|
||||
if(frm.doc.session_status=="Expired"){
|
||||
frm.dashboard.set_headline_alert(
|
||||
'<div class="row">' +
|
||||
'<div class="col-xs-12">' +
|
||||
'<span class="indicator whitespace-nowrap red'+ '' +'"><span class="hidden-xs">Session Not Active. Save doc to login.</span></span> ' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
if(frm.doc.session_status=="Active"){
|
||||
let d = new Date(frm.doc.modified)
|
||||
d.setDate(d.getDate()+60);
|
||||
let dn = new Date()
|
||||
let days = d.getTime() - dn.getTime();
|
||||
days = Math.floor(days/(1000 * 3600 * 24));
|
||||
let msg,color;
|
||||
if(days>0){
|
||||
msg = "Your Session will be expire in " + days + " days.";
|
||||
color = "green";
|
||||
}
|
||||
else{
|
||||
msg = "Session is expired. Save doc to login.";
|
||||
color = "red";
|
||||
}
|
||||
frm.dashboard.set_headline_alert(
|
||||
'<div class="row">' +
|
||||
'<div class="col-xs-12">' +
|
||||
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">' + msg + ' </span></span> ' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
},
|
||||
login: function(frm){
|
||||
if(frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "get_authorization_url",
|
||||
callback : function(r) {
|
||||
window.location.href = r.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
after_save: function(frm){
|
||||
frm.trigger("login");
|
||||
}
|
||||
});
|
111
erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
Normal file
111
erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
Normal file
@ -0,0 +1,111 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2020-01-30 13:36:39.492931",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_name",
|
||||
"column_break_2",
|
||||
"company_id",
|
||||
"oauth_details",
|
||||
"consumer_key",
|
||||
"column_break_5",
|
||||
"consumer_secret",
|
||||
"user_details_section",
|
||||
"access_token",
|
||||
"person_urn",
|
||||
"session_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "account_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "oauth_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "OAuth Credentials"
|
||||
},
|
||||
{
|
||||
"fieldname": "consumer_key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Consumer Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "consumer_secret",
|
||||
"fieldtype": "Password",
|
||||
"in_list_view": 1,
|
||||
"label": "Consumer Secret",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Access Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "person_urn",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Person URN",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "user_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "User Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "session_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Session Status",
|
||||
"options": "Expired\nActive",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Company ID",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-16 23:22:51.966397",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "LinkedIn Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
157
erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
Normal file
157
erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
Normal file
@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, requests, json
|
||||
from frappe import _
|
||||
from frappe.utils import get_site_url, get_url_to_form, get_link_to_form
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.file_manager import get_file, get_file_path
|
||||
from six.moves.urllib.parse import urlencode
|
||||
class LinkedInSettings(Document):
|
||||
def get_authorization_url(self):
|
||||
params = urlencode({
|
||||
"response_type":"code",
|
||||
"client_id": self.consumer_key,
|
||||
"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback",
|
||||
"scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social"
|
||||
})
|
||||
|
||||
url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params)
|
||||
|
||||
return url
|
||||
|
||||
def get_access_token(self, code):
|
||||
url = "https://www.linkedin.com/oauth/v2/accessToken"
|
||||
body = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": self.consumer_key,
|
||||
"client_secret": self.get_password(fieldname="consumer_secret"),
|
||||
"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback",
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
response = self.http_post(url=url, data=body, headers=headers)
|
||||
response = frappe.parse_json(response.content.decode())
|
||||
self.db_set("access_token", response["access_token"])
|
||||
|
||||
def get_member_profile(self):
|
||||
headers = {
|
||||
"Authorization": "Bearer {}".format(self.access_token)
|
||||
}
|
||||
url = "https://api.linkedin.com/v2/me"
|
||||
response = requests.get(url=url, headers=headers)
|
||||
response = frappe.parse_json(response.content.decode())
|
||||
self.db_set("person_urn", response["id"])
|
||||
self.db_set("account_name", response["vanityName"])
|
||||
self.db_set("session_status", "Active")
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
|
||||
|
||||
def post(self, text, media=None):
|
||||
if not media:
|
||||
return self.post_text(text)
|
||||
else:
|
||||
media_id = self.upload_image(media)
|
||||
if media_id:
|
||||
return self.post_text(text, media_id=media_id)
|
||||
else:
|
||||
frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
|
||||
|
||||
|
||||
def upload_image(self, media):
|
||||
media = get_file_path(media)
|
||||
register_url = "https://api.linkedin.com/v2/assets?action=registerUpload"
|
||||
|
||||
body = {
|
||||
"registerUploadRequest": {
|
||||
"recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
|
||||
"owner": "urn:li:organization:{0}".format(self.company_id),
|
||||
"serviceRelationships": [{
|
||||
"relationshipType": "OWNER",
|
||||
"identifier": "urn:li:userGeneratedContent"
|
||||
}]
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
"Authorization": "Bearer {}".format(self.access_token)
|
||||
}
|
||||
response = self.http_post(url=register_url, body=body, headers=headers)
|
||||
if response.status_code == 200:
|
||||
response = response.json()
|
||||
asset = response["value"]["asset"]
|
||||
upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"]
|
||||
headers['Content-Type']='image/jpeg'
|
||||
response = self.http_post(upload_url, headers=headers, data=open(media,"rb"))
|
||||
if response.status_code < 200 and response.status_code > 299:
|
||||
frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason))
|
||||
return None
|
||||
return asset
|
||||
return None
|
||||
|
||||
|
||||
def post_text(self, text, media_id=None):
|
||||
# url = "https://api.linkedin.com/v2/ugcPosts"
|
||||
url = "https://api.linkedin.com/v2/shares"
|
||||
headers = {
|
||||
"X-Restli-Protocol-Version": "2.0.0",
|
||||
"Authorization": "Bearer {}".format(self.access_token),
|
||||
"Content-Type": "application/json; charset=UTF-8"
|
||||
}
|
||||
body = {
|
||||
"distribution": {
|
||||
"linkedInDistributionTarget": {}
|
||||
},
|
||||
"owner":"urn:li:organization:{0}".format(self.company_id),
|
||||
"subject": "Test Share Subject",
|
||||
"text": {
|
||||
"text": text
|
||||
}
|
||||
}
|
||||
if media_id:
|
||||
body["content"]= {
|
||||
"contentEntities": [{
|
||||
"entity": media_id
|
||||
}],
|
||||
"shareMediaCategory": "IMAGE"
|
||||
}
|
||||
response = self.http_post(url=url, headers=headers, body=body)
|
||||
return response
|
||||
|
||||
def http_post(self, url, headers=None, body=None, data=None):
|
||||
try:
|
||||
response = requests.post(
|
||||
url = url,
|
||||
json = body,
|
||||
data = data,
|
||||
headers = headers
|
||||
)
|
||||
if response.status_code not in [201,200]:
|
||||
raise
|
||||
except Exception as e:
|
||||
content = json.loads(response.content)
|
||||
if response.status_code == 401:
|
||||
self.db_set("session_status", "Expired")
|
||||
frappe.db.commit()
|
||||
frappe.throw(content["message"], title="LinkedIn Error - Unauthorized")
|
||||
elif response.status_code == 403:
|
||||
frappe.msgprint(_("You Didn't have permission to access this API"))
|
||||
frappe.throw(content["message"], title="LinkedIn Error - Access Denied")
|
||||
else:
|
||||
frappe.throw(response.reason, title=response.status_code)
|
||||
return response
|
||||
|
||||
@frappe.whitelist()
|
||||
def callback(code=None, error=None, error_description=None):
|
||||
if not error:
|
||||
linkedin_settings = frappe.get_doc("LinkedIn Settings")
|
||||
linkedin_settings.get_access_token(code)
|
||||
linkedin_settings.get_member_profile()
|
||||
frappe.db.commit()
|
||||
else:
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLinkedInSettings(unittest.TestCase):
|
||||
pass
|
0
erpnext/crm/doctype/social_media_post/__init__.py
Normal file
0
erpnext/crm/doctype/social_media_post/__init__.py
Normal file
54
erpnext/crm/doctype/social_media_post/social_media_post.js
Normal file
54
erpnext/crm/doctype/social_media_post/social_media_post.js
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
frappe.ui.form.on('Social Media Post', {
|
||||
validate: function(frm){
|
||||
if(frm.doc.text.length > 280){
|
||||
frappe.throw("Length Must be less than 280.")
|
||||
}
|
||||
},
|
||||
refresh: function(frm){
|
||||
if(frm.doc.docstatus === 1){
|
||||
if(frm.doc.post_status != "Posted"){
|
||||
add_post_btn(frm);
|
||||
}
|
||||
else if(frm.doc.post_status == "Posted"){
|
||||
frm.set_df_property('sheduled_time', 'read_only', 1);
|
||||
}
|
||||
let html = '<div class="row">';
|
||||
if(frm.doc.twitter){
|
||||
let color = frm.doc.twitter_post_id ? "green" : "red";
|
||||
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
|
||||
html += '<div class="col-xs-6">' +
|
||||
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">Twitter : '+ status +'</span></span> ' +
|
||||
'</div>' ;
|
||||
}
|
||||
if(frm.doc.linkedin){
|
||||
let color = frm.doc.linkedin_post_id ? "green" : "red";
|
||||
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
|
||||
html += '<div class="col-xs-6">' +
|
||||
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">LinkedIn : '+ status +'</span></span> ' +
|
||||
'</div>' ;
|
||||
}
|
||||
html += '</div>';
|
||||
frm.dashboard.set_headline_alert(html);
|
||||
}
|
||||
}
|
||||
});
|
||||
var add_post_btn = function(frm){
|
||||
frm.add_custom_button(('Post Now'), function(){
|
||||
post(frm);
|
||||
});
|
||||
}
|
||||
var post = function(frm){
|
||||
frappe.call({
|
||||
method: "erpnext.crm.doctype.social_media_post.social_media_post.publish",
|
||||
args: {
|
||||
doctype: frm.doc.doctype,
|
||||
name: frm.doc.name
|
||||
},
|
||||
callback: function(r) {
|
||||
frm.reload_doc();
|
||||
}
|
||||
})
|
||||
|
||||
}
|
157
erpnext/crm/doctype/social_media_post/social_media_post.json
Normal file
157
erpnext/crm/doctype/social_media_post/social_media_post.json
Normal file
@ -0,0 +1,157 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
|
||||
"creation": "2020-01-30 11:53:13.872864",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"campaign_name",
|
||||
"sheduled_time",
|
||||
"post_status",
|
||||
"column_break_6",
|
||||
"twitter",
|
||||
"linkedin",
|
||||
"twitter_post_id",
|
||||
"linkedin_post_id",
|
||||
"content",
|
||||
"text",
|
||||
"column_break_14",
|
||||
"tweet_preview",
|
||||
"linkedin_section",
|
||||
"linkedin_post",
|
||||
"attachments_section",
|
||||
"image",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "text",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Tweet",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "twitter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Twitter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "linkedin",
|
||||
"fieldtype": "Check",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Social Media Post",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sheduled_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Scheduled Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Twitter"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "post_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Post Status",
|
||||
"options": "\nScheduled\nPosted\nError",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "twitter_post_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Twitter Post Id",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "linkedin_post_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "LinkedIn Post Id",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "campaign_name",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Campaign",
|
||||
"options": "Campaign"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break",
|
||||
"label": "Share On"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "tweet_preview",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "linkedin_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "attachments_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Attachments"
|
||||
},
|
||||
{
|
||||
"fieldname": "linkedin_post",
|
||||
"fieldtype": "Text",
|
||||
"label": "Post"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-09 22:35:23.821991",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Social Media Post",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
49
erpnext/crm/doctype/social_media_post/social_media_post.py
Normal file
49
erpnext/crm/doctype/social_media_post/social_media_post.py
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
|
||||
class SocialMediaPost(Document):
|
||||
def submit(self):
|
||||
if self.sheduled_time:
|
||||
self.post_status = "Scheduled"
|
||||
super(SocialMediaPost, self).submit()
|
||||
|
||||
def post(self):
|
||||
try:
|
||||
if self.twitter and not self.twitter_post_id:
|
||||
twitter = frappe.get_doc("Twitter Settings")
|
||||
twitter_post = twitter.post(self.text, self.image)
|
||||
self.db_set("twitter_post_id", twitter_post.id)
|
||||
if self.linkedin and not self.linkedin_post_id:
|
||||
linkedin = frappe.get_doc("LinkedIn Settings")
|
||||
linkedin_post = linkedin.post(self.text, self.image)
|
||||
self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1])
|
||||
self.db_set("post_status", "Posted")
|
||||
|
||||
except Exception as e:
|
||||
self.db_set("post_status", "Error")
|
||||
title = _("Error while POSTING {0}").format(self.name)
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(message=traceback , title=title)
|
||||
|
||||
def process_scheduled_social_media_posts():
|
||||
import datetime
|
||||
posts = frappe.get_list("Social Media Post", filters={"status": "Scheduled"}, fields= ["name", "sheduled_time"])
|
||||
start = frappe.utils.now_datetime()
|
||||
end = start + datetime.timedelta(minutes=59)
|
||||
for post in posts:
|
||||
post_time = frappe.utils.get_datetime(post.scheduled_time)
|
||||
if post_time > start and post_time <= end:
|
||||
post = frappe.get_doc('Social Media Post',post['name'])
|
||||
post.post()
|
||||
|
||||
@frappe.whitelist()
|
||||
def publish(doctype, name):
|
||||
sm_post = frappe.get_doc(doctype, name)
|
||||
sm_post.post()
|
||||
frappe.db.commit()
|
@ -0,0 +1,10 @@
|
||||
frappe.listview_settings['Social Media Post'] = {
|
||||
add_fields: ["status","post_status"],
|
||||
get_indicator: function(doc) {
|
||||
return [__(doc.post_status), {
|
||||
"Scheduled": "orange",
|
||||
"Posted": "green",
|
||||
"Error": "red"
|
||||
}[doc.post_status]];
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestSocialMediaPost(unittest.TestCase):
|
||||
pass
|
0
erpnext/crm/doctype/twitter_settings/__init__.py
Normal file
0
erpnext/crm/doctype/twitter_settings/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestTwitterSettings(unittest.TestCase):
|
||||
pass
|
52
erpnext/crm/doctype/twitter_settings/twitter_settings.js
Normal file
52
erpnext/crm/doctype/twitter_settings/twitter_settings.js
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Twitter Settings', {
|
||||
onload: function(frm){
|
||||
if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||
frappe.confirm(
|
||||
'Session not valid, Do you want to login?',
|
||||
function(){
|
||||
frm.trigger("login");
|
||||
},
|
||||
function(){
|
||||
window.close();
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
refresh: function(frm){
|
||||
if(frm.doc.session_status=="Active"){
|
||||
frm.dashboard.set_headline_alert(
|
||||
'<div class="row">' +
|
||||
'<div class="col-xs-12">' +
|
||||
'<span class="indicator whitespace-nowrap green'+ '' +'"><span class="hidden-xs">Session Active</span></span> ' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
else if(frm.doc.session_status=="Expired"){
|
||||
frm.dashboard.set_headline_alert(
|
||||
'<div class="row">' +
|
||||
'<div class="col-xs-12">' +
|
||||
'<span class="indicator whitespace-nowrap red'+ '' +'"><span class="hidden-xs">Session Not Active. Save doc to login.</span></span> ' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
},
|
||||
login: function(frm){
|
||||
if(frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "get_authorize_url",
|
||||
callback : function(r) {
|
||||
window.location.href = r.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
after_save: function(frm){
|
||||
frm.trigger("login");
|
||||
}
|
||||
});
|
101
erpnext/crm/doctype/twitter_settings/twitter_settings.json
Normal file
101
erpnext/crm/doctype/twitter_settings/twitter_settings.json
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2020-01-30 10:29:08.562108",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_name",
|
||||
"profile_pic",
|
||||
"oauth_details",
|
||||
"consumer_key",
|
||||
"column_break_5",
|
||||
"consumer_secret",
|
||||
"oauth_token",
|
||||
"oauth_secret",
|
||||
"session_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "account_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "oauth_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "OAuth Credentials"
|
||||
},
|
||||
{
|
||||
"fieldname": "consumer_key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Consumer Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "consumer_secret",
|
||||
"fieldtype": "Password",
|
||||
"in_list_view": 1,
|
||||
"label": "Consumer Secret Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "oauth_token",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "OAuth Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "oauth_secret",
|
||||
"fieldtype": "Password",
|
||||
"hidden": 1,
|
||||
"label": "OAuth Token Secret",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "profile_pic",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "session_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Session Status",
|
||||
"options": "Expired\nActive",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"image_field": "profile_pic",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-08 23:56:20.621246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Twitter Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
90
erpnext/crm/doctype/twitter_settings/twitter_settings.py
Normal file
90
erpnext/crm/doctype/twitter_settings/twitter_settings.py
Normal file
@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, os, tweepy, json
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.file_manager import get_file_path
|
||||
from frappe.utils import get_url_to_form, get_link_to_form
|
||||
from tweepy.error import TweepError
|
||||
|
||||
class TwitterSettings(Document):
|
||||
def get_authorize_url(self):
|
||||
callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url())
|
||||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url)
|
||||
try:
|
||||
redirect_url = auth.get_authorization_url()
|
||||
return redirect_url
|
||||
except:
|
||||
frappe.msgprint(_("Error! Failed to get request token."))
|
||||
frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")))
|
||||
|
||||
|
||||
def get_access_token(self, oauth_token, oauth_verifier):
|
||||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
|
||||
auth.request_token = {
|
||||
'oauth_token' : oauth_token,
|
||||
'oauth_token_secret' : oauth_verifier
|
||||
}
|
||||
try:
|
||||
auth.get_access_token(oauth_verifier)
|
||||
self.db_set("oauth_token", auth.access_token)
|
||||
self.db_set("oauth_secret", auth.access_token_secret)
|
||||
api = self.get_api()
|
||||
user = api.me()
|
||||
self.db_set("account_name", user._json["screen_name"])
|
||||
profile_pic = (user._json["profile_image_url"]).replace("_normal","")
|
||||
self.db_set("profile_pic", profile_pic)
|
||||
self.db_set("session_status", "Active")
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings")
|
||||
except TweepError as e:
|
||||
frappe.msgprint(_("Error! Failed to get access token."))
|
||||
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
|
||||
|
||||
def get_api(self):
|
||||
# authentication of consumer key and secret
|
||||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
|
||||
# authentication of access token and secret
|
||||
auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret"))
|
||||
|
||||
return tweepy.API(auth)
|
||||
|
||||
def post(self, text, media=None):
|
||||
if not media:
|
||||
return self.send_tweet(text)
|
||||
|
||||
if media:
|
||||
media_id = self.upload_image(media)
|
||||
return self.send_tweet(text, media_id)
|
||||
|
||||
def upload_image(self, media):
|
||||
media = get_file_path(media)
|
||||
api = self.get_api()
|
||||
media = api.media_upload(media)
|
||||
return media.media_id
|
||||
|
||||
def send_tweet(self, text, media_id=None):
|
||||
api = self.get_api()
|
||||
try:
|
||||
if media_id:
|
||||
response = api.update_status(status = text, media_ids = [media_id])
|
||||
else:
|
||||
response = api.update_status(status = text)
|
||||
return response
|
||||
|
||||
except TweepError as e:
|
||||
content = json.loads(e.response.content)
|
||||
content = content["errors"][0]
|
||||
if e.response.status_code == 401:
|
||||
self.db_set("session_status", "Expired")
|
||||
frappe.db.commit()
|
||||
frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason))
|
||||
|
||||
@frappe.whitelist()
|
||||
def callback(oauth_token, oauth_verifier):
|
||||
twitter_settings = frappe.get_single("Twitter Settings")
|
||||
twitter_settings.get_access_token(oauth_token,oauth_verifier)
|
||||
frappe.db.commit()
|
@ -270,7 +270,8 @@ auto_cancel_exempted_doctypes= [
|
||||
scheduler_events = {
|
||||
"all": [
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
"erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder"
|
||||
"erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
|
||||
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
|
||||
],
|
||||
"hourly": [
|
||||
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
|
||||
|
@ -8,6 +8,10 @@ def get_data():
|
||||
{
|
||||
'label': _('Email Campaigns'),
|
||||
'items': ['Email Campaign']
|
||||
},
|
||||
{
|
||||
'label': _('Social Media Campaigns'),
|
||||
'items': ['Social Media Post']
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
|
@ -8,3 +8,4 @@ PyGithub==1.44.1
|
||||
python-stdnum==1.12
|
||||
Unidecode==1.1.1
|
||||
WooCommerce==2.1.1
|
||||
tweepy==3.8.0
|
Loading…
x
Reference in New Issue
Block a user